33import React , { useState , useMemo , useEffect } from 'react' ;
44import { Card , CardContent , CardHeader , CardTitle } from '../ui/card' ;
55import { 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' ;
87import { formatUsdCents } from '../../utils/formatCurrency' ;
98import { useUserEarnings } from '../../hooks/useUserEarnings' ;
9+ import { toast } from '../ui/use-toast' ;
10+ import { PillLink } from '../utils/PillLink' ;
1011
1112interface 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