@@ -14,25 +14,55 @@ type CampaignRow = {
1414 created_at : string ;
1515} ;
1616
17+ type StatRow = {
18+ campaign_id : string ;
19+ impressions : number ;
20+ clicks : number ;
21+ spent_cents : number ;
22+ spend_today_cents : number ;
23+ total_spent_cents : number ;
24+ } ;
25+
1726function dollars ( cents : number ) : string {
1827 return `$${ ( cents / 100 ) . toFixed ( 2 ) } ` ;
1928}
2029
30+ function ctr ( clicks : number , impressions : number ) : string {
31+ if ( ! impressions ) return "—" ;
32+ return `${ ( ( clicks / impressions ) * 100 ) . toFixed ( 1 ) } %` ;
33+ }
34+
2135export default async function AdsPage ( ) {
2236 const supabase = await createClient ( ) ;
2337 const {
2438 data : { user } ,
2539 } = await supabase . auth . getUser ( ) ;
2640
2741 let campaigns : CampaignRow [ ] = [ ] ;
42+ const statsById = new Map < string , StatRow > ( ) ;
2843 if ( user ) {
29- const { data } = await supabase
30- . from ( "ad_campaigns" )
31- . select ( "id, ref_slug, name, destination_domain, daily_budget_cents, status, created_at" )
32- . order ( "created_at" , { ascending : false } ) ;
44+ const [ { data } , { data : stats } ] = await Promise . all ( [
45+ supabase
46+ . from ( "ad_campaigns" )
47+ . select ( "id, ref_slug, name, destination_domain, daily_budget_cents, status, created_at" )
48+ . order ( "created_at" , { ascending : false } ) ,
49+ supabase
50+ . from ( "ad_campaign_stats" )
51+ . select ( "campaign_id, impressions, clicks, spent_cents, spend_today_cents, total_spent_cents" ) ,
52+ ] ) ;
3353 campaigns = ( data as CampaignRow [ ] ) ?? [ ] ;
54+ for ( const s of ( stats as StatRow [ ] ) ?? [ ] ) statsById . set ( s . campaign_id , s ) ;
3455 }
3556
57+ const totals = [ ...statsById . values ( ) ] . reduce (
58+ ( a , s ) => ( {
59+ impressions : a . impressions + ( s . impressions ?? 0 ) ,
60+ clicks : a . clicks + ( s . clicks ?? 0 ) ,
61+ spent : a . spent + ( s . total_spent_cents ?? 0 ) ,
62+ } ) ,
63+ { impressions : 0 , clicks : 0 , spent : 0 } ,
64+ ) ;
65+
3666 return (
3767 < div className = "mx-auto max-w-4xl" >
3868 < div className = "flex items-center justify-between" >
@@ -51,6 +81,15 @@ export default async function AdsPage() {
5181 you fund a daily budget — publishers earn crypto for the clicks.
5282 </ p >
5383
84+ { campaigns . length > 0 && (
85+ < div className = "mt-6 grid grid-cols-2 gap-3 sm:grid-cols-4" >
86+ < Stat label = "Impressions" value = { totals . impressions . toLocaleString ( ) } />
87+ < Stat label = "Clicks" value = { totals . clicks . toLocaleString ( ) } />
88+ < Stat label = "CTR" value = { ctr ( totals . clicks , totals . impressions ) } />
89+ < Stat label = "Total spend" value = { dollars ( totals . spent ) } />
90+ </ div >
91+ ) }
92+
5493 { campaigns . length === 0 ? (
5594 < div className = "card mt-6 p-8 text-center text-[var(--color-muted)]" >
5695 No campaigns yet.{ " " }
@@ -60,27 +99,59 @@ export default async function AdsPage() {
6099 .
61100 </ div >
62101 ) : (
63- < ul className = "mt-6 space-y-2" >
64- { campaigns . map ( ( c ) => (
65- < li key = { c . id } className = "card flex items-center justify-between gap-4 p-4" >
66- < div className = "min-w-0" >
67- < div className = "truncate font-semibold" > { c . name } </ div >
68- < div className = "truncate text-sm text-[var(--color-muted)]" >
69- { c . destination_domain } · { dollars ( c . daily_budget_cents ) } /day ·{ " " }
70- < span className = "font-mono" > { c . ref_slug } </ span >
102+ < ul className = "mt-4 space-y-2" >
103+ { campaigns . map ( ( c ) => {
104+ const s = statsById . get ( c . id ) ;
105+ const impr = s ?. impressions ?? 0 ;
106+ const clk = s ?. clicks ?? 0 ;
107+ return (
108+ < li key = { c . id } className = "card p-4" >
109+ < div className = "flex items-center justify-between gap-4" >
110+ < div className = "min-w-0" >
111+ < div className = "truncate font-semibold" > { c . name } </ div >
112+ < div className = "truncate text-sm text-[var(--color-muted)]" >
113+ { c . destination_domain } · { dollars ( c . daily_budget_cents ) } /day ·{ " " }
114+ < span className = "font-mono" > { c . ref_slug } </ span >
115+ </ div >
116+ </ div >
117+ < div className = "flex items-center gap-3" >
118+ < span className = "badge whitespace-nowrap" > { c . status } </ span >
119+ < Link href = { `/ads/${ c . id } ` } className = "btn text-sm" >
120+ Edit
121+ </ Link >
122+ < CampaignActions id = { c . id } status = { c . status } />
123+ </ div >
124+ </ div >
125+ < div className = "mt-3 flex flex-wrap gap-x-6 gap-y-1 border-t border-[var(--color-border)] pt-3 text-sm" >
126+ < MiniStat label = "Impressions" value = { impr . toLocaleString ( ) } />
127+ < MiniStat label = "Clicks" value = { clk . toLocaleString ( ) } />
128+ < MiniStat label = "CTR" value = { ctr ( clk , impr ) } />
129+ < MiniStat label = "Spent" value = { dollars ( s ?. total_spent_cents ?? 0 ) } />
130+ < MiniStat label = "Today" value = { dollars ( s ?. spend_today_cents ?? 0 ) } />
71131 </ div >
72- </ div >
73- < div className = "flex items-center gap-3" >
74- < span className = "badge whitespace-nowrap" > { c . status } </ span >
75- < Link href = { `/ads/${ c . id } ` } className = "btn text-sm" >
76- Edit
77- </ Link >
78- < CampaignActions id = { c . id } status = { c . status } />
79- </ div >
80- </ li >
81- ) ) }
132+ </ li >
133+ ) ;
134+ } ) }
82135 </ ul >
83136 ) }
84137 </ div >
85138 ) ;
86139}
140+
141+ function Stat ( { label, value } : { label : string ; value : string } ) {
142+ return (
143+ < div className = "card p-4" >
144+ < div className = "text-xs uppercase tracking-wider text-[var(--color-muted)]" > { label } </ div >
145+ < div className = "mt-1 text-2xl font-bold" > { value } </ div >
146+ </ div >
147+ ) ;
148+ }
149+
150+ function MiniStat ( { label, value } : { label : string ; value : string } ) {
151+ return (
152+ < span >
153+ < span className = "text-[var(--color-muted)]" > { label } : </ span >
154+ < span className = "font-mono font-semibold" > { value } </ span >
155+ </ span >
156+ ) ;
157+ }
0 commit comments