Skip to content

Commit ccf9f6d

Browse files
committed
feat: mobile-optimize earnings sources UI with proper pill links
- Remove outer card wrapper, convert to page subheader layout - Create individual cards for each earning page/sponsor - Replace Badge components with proper PillLink components - Remove external link icons, use clean pill styling - Add copy link functionality with toast feedback - Respect user's link appearance settings (filled/outline/text-only/underlined) - Improve mobile UX with larger touch targets and better spacing
1 parent 2decc06 commit ccf9f6d

File tree

1 file changed

+128
-80
lines changed

1 file changed

+128
-80
lines changed

app/components/payments/EarningsSourceBreakdown.tsx

Lines changed: 128 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
import React, { useState, useMemo, useEffect } from 'react';
44
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
55
import { Button } from '../ui/button';
6-
import { Badge } from '../ui/badge';
7-
import { FileText, Users, TrendingUp } from 'lucide-react';
6+
import { FileText, Users, TrendingUp, Copy } from 'lucide-react';
87
import { formatUsdCents } from '../../utils/formatCurrency';
98
import { useUserEarnings } from '../../hooks/useUserEarnings';
9+
import { toast } from '../ui/use-toast';
10+
import { PillLink } from '../utils/PillLink';
1011

1112
interface PageEarning {
1213
pageId: string;
@@ -28,7 +29,7 @@ type BreakdownMode = 'pages' | 'sponsors';
2829

2930
/**
3031
* EarningsSourceBreakdown - Shows where earnings are coming from
31-
*
32+
*
3233
* Two modes:
3334
* - Pages: Shows which pages are earning the most
3435
* - Sponsors: Shows which users are contributing the most
@@ -39,6 +40,25 @@ export default function EarningsSourceBreakdown() {
3940
const [historicalEarnings, setHistoricalEarnings] = useState<any[]>([]);
4041
const [loadingHistorical, setLoadingHistorical] = useState(false);
4142

43+
// Copy page link to clipboard
44+
const copyPageLink = async (pageId: string, pageTitle: string) => {
45+
try {
46+
const url = `${window.location.origin}/${pageId}`;
47+
await navigator.clipboard.writeText(url);
48+
toast({
49+
title: "Link copied!",
50+
description: `Copied link for "${pageTitle}"`,
51+
});
52+
} catch (err) {
53+
console.error('Failed to copy link:', err);
54+
toast({
55+
title: "Copy failed",
56+
description: "Could not copy link to clipboard",
57+
variant: "destructive",
58+
});
59+
}
60+
};
61+
4262
// Load historical earnings data to show sources for available balance
4363
useEffect(() => {
4464
if (earnings?.availableBalance > 0 && !loadingHistorical) {
@@ -168,77 +188,98 @@ export default function EarningsSourceBreakdown() {
168188

169189
if (loading) {
170190
return (
171-
<Card>
172-
<CardHeader>
191+
<div className="space-y-4">
192+
{/* Header */}
193+
<div className="flex items-center justify-between">
173194
<div className="h-6 w-48 bg-muted rounded animate-pulse" />
174-
</CardHeader>
175-
<CardContent>
176-
<div className="space-y-3">
177-
{[...Array(3)].map((_, i) => (
178-
<div key={i} className="flex justify-between items-center">
179-
<div className="h-4 w-32 bg-muted rounded animate-pulse" />
195+
<div className="h-8 w-32 bg-muted rounded animate-pulse" />
196+
</div>
197+
198+
{/* Loading cards */}
199+
<div className="space-y-3">
200+
{[...Array(3)].map((_, i) => (
201+
<Card key={i} className="p-4">
202+
<div className="flex justify-between items-center">
203+
<div className="flex-1 space-y-2">
204+
<div className="h-4 w-32 bg-muted rounded animate-pulse" />
205+
<div className="h-3 w-20 bg-muted rounded animate-pulse" />
206+
</div>
180207
<div className="h-4 w-16 bg-muted rounded animate-pulse" />
181208
</div>
182-
))}
183-
</div>
184-
</CardContent>
185-
</Card>
209+
</Card>
210+
))}
211+
</div>
212+
</div>
186213
);
187214
}
188215

189216
const hasEarnings = pageBreakdown.length > 0 || sponsorBreakdown.length > 0;
190217

191218
return (
192-
<Card>
193-
<CardHeader>
194-
<div className="flex items-center justify-between">
195-
<CardTitle className="flex items-center gap-2">
196-
<TrendingUp className="h-5 w-5 text-green-600" />
197-
Earnings Sources
198-
</CardTitle>
199-
200-
{hasEarnings && (
201-
<div className="flex gap-1">
202-
<Button
203-
variant={mode === 'pages' ? 'default' : 'outline'}
204-
size="sm"
205-
onClick={() => setMode('pages')}
206-
className="flex items-center gap-1"
207-
>
208-
<FileText className="h-3 w-3" />
209-
Pages
210-
</Button>
211-
<Button
212-
variant={mode === 'sponsors' ? 'default' : 'outline'}
213-
size="sm"
214-
onClick={() => setMode('sponsors')}
215-
className="flex items-center gap-1"
216-
>
217-
<Users className="h-3 w-3" />
218-
Sponsors
219-
</Button>
220-
</div>
221-
)}
222-
</div>
223-
</CardHeader>
224-
225-
<CardContent>
226-
{!hasEarnings ? (
227-
<div className="text-center py-8 text-muted-foreground">
228-
<TrendingUp className="h-12 w-12 mx-auto mb-3 opacity-50" />
229-
<p className="text-sm">No current earnings sources</p>
230-
<p className="text-xs mt-1">Start writing pages to earn from supporters</p>
219+
<div className="space-y-4">
220+
{/* Page Subheader */}
221+
<div className="flex items-center justify-between">
222+
<h2 className="text-lg font-semibold flex items-center gap-2">
223+
<TrendingUp className="h-5 w-5 text-green-600" />
224+
Earnings Sources
225+
</h2>
226+
227+
{hasEarnings && (
228+
<div className="flex gap-1">
229+
<Button
230+
variant={mode === 'pages' ? 'default' : 'outline'}
231+
size="sm"
232+
onClick={() => setMode('pages')}
233+
className="flex items-center gap-1"
234+
>
235+
<FileText className="h-3 w-3" />
236+
Pages
237+
</Button>
238+
<Button
239+
variant={mode === 'sponsors' ? 'default' : 'outline'}
240+
size="sm"
241+
onClick={() => setMode('sponsors')}
242+
className="flex items-center gap-1"
243+
>
244+
<Users className="h-3 w-3" />
245+
Sponsors
246+
</Button>
231247
</div>
232-
) : (
233-
<div className="space-y-3">
234-
{mode === 'pages' ? (
235-
// Pages breakdown
236-
pageBreakdown.map((page, index) => (
237-
<div key={page.pageId} className="flex items-center justify-between p-3 rounded-lg bg-muted/30">
248+
)}
249+
</div>
250+
251+
{/* Content */}
252+
{!hasEarnings ? (
253+
<div className="text-center py-8 text-muted-foreground">
254+
<TrendingUp className="h-12 w-12 mx-auto mb-3 opacity-50" />
255+
<p className="text-sm">No current earnings sources</p>
256+
<p className="text-xs mt-1">Start writing pages to earn from supporters</p>
257+
</div>
258+
) : (
259+
<div className="space-y-3">
260+
{mode === 'pages' ? (
261+
// Pages breakdown - Each page gets its own card
262+
pageBreakdown.map((page, index) => (
263+
<Card key={page.pageId} className="p-4">
264+
<div className="flex items-center justify-between">
238265
<div className="flex-1 min-w-0">
239-
<div className="flex items-center gap-2">
266+
<div className="flex items-center gap-2 mb-2">
240267
<span className="text-xs text-muted-foreground font-mono">#{index + 1}</span>
241-
<h4 className="font-medium truncate">{page.pageTitle}</h4>
268+
<PillLink
269+
href={`/${page.pageId}`}
270+
pageId={page.pageId}
271+
isPublic={true}
272+
>
273+
{page.pageTitle}
274+
</PillLink>
275+
<Button
276+
variant="ghost"
277+
size="sm"
278+
className="h-6 w-6 p-0"
279+
onClick={() => copyPageLink(page.pageId, page.pageTitle)}
280+
>
281+
<Copy className="h-3 w-3" />
282+
</Button>
242283
</div>
243284
<p className="text-xs text-muted-foreground">
244285
{page.sponsorCount} sponsor{page.sponsorCount !== 1 ? 's' : ''}
@@ -251,27 +292,35 @@ export default function EarningsSourceBreakdown() {
251292
<div className="text-xs text-muted-foreground">/month</div>
252293
</div>
253294
</div>
254-
))
295+
</Card>
296+
))
255297
) : (
256-
// Sponsors breakdown
298+
// Sponsors breakdown - Each sponsor gets its own card
257299
sponsorBreakdown.map((sponsor, index) => (
258-
<div key={sponsor.userId} className="flex items-center justify-between p-3 rounded-lg bg-muted/30">
259-
<div className="flex-1 min-w-0">
260-
<div className="flex items-center gap-2">
261-
<span className="text-xs text-muted-foreground font-mono">#{index + 1}</span>
262-
<h4 className="font-medium truncate">{sponsor.username}</h4>
300+
<Card key={sponsor.userId} className="p-4">
301+
<div className="flex items-center justify-between">
302+
<div className="flex-1 min-w-0">
303+
<div className="flex items-center gap-2 mb-2">
304+
<span className="text-xs text-muted-foreground font-mono">#{index + 1}</span>
305+
<PillLink
306+
href={`/user/${sponsor.userId}`}
307+
isPublic={true}
308+
>
309+
{sponsor.username}
310+
</PillLink>
311+
</div>
312+
<p className="text-xs text-muted-foreground">
313+
Supporting {sponsor.pageCount} page{sponsor.pageCount !== 1 ? 's' : ''}
314+
</p>
263315
</div>
264-
<p className="text-xs text-muted-foreground">
265-
Supporting {sponsor.pageCount} page{sponsor.pageCount !== 1 ? 's' : ''}
266-
</p>
267-
</div>
268-
<div className="text-right">
269-
<div className="font-semibold text-green-600">
270-
{formatUsdCents(sponsor.totalContribution * 100)}
316+
<div className="text-right">
317+
<div className="font-semibold text-green-600">
318+
{formatUsdCents(sponsor.totalContribution * 100)}
319+
</div>
320+
<div className="text-xs text-muted-foreground">/month</div>
271321
</div>
272-
<div className="text-xs text-muted-foreground">/month</div>
273322
</div>
274-
</div>
323+
</Card>
275324
))
276325
)}
277326

@@ -291,7 +340,6 @@ export default function EarningsSourceBreakdown() {
291340
</div>
292341
</div>
293342
)}
294-
</CardContent>
295-
</Card>
343+
</div>
296344
);
297345
}

0 commit comments

Comments
 (0)