11import Link from "next/link" ;
2- import Image from "next/image" ;
32import { cookies } from "next/headers" ;
43import { redirect } from "next/navigation" ;
5- import { Eye , MousePointerClick , Plus , ShoppingBag , Sparkles } from "lucide-react" ;
6- import type { Cart , User } from "@/lib/types" ;
7- import { getCurrentUser , listMyCarts } from "@/lib/api-client" ;
4+ import { Plus , Sparkles } from "lucide-react" ;
5+ import type { Cart , DailyStat , User } from "@/lib/types" ;
6+ import { getCurrentUser , getInsights , listMyCarts } from "@/lib/api-client" ;
87import { CartCard } from "@/components/cart-card" ;
9- import { AnimatedNumber } from "@/components/animated-number" ;
108import { InstallNudge } from "@/components/install-nudge" ;
9+ import { DashboardHero } from "@/components/dashboard-hero" ;
10+ import { InsightSummary } from "@/components/insight-summary" ;
11+ import { TopCartCard } from "@/components/top-cart-card" ;
12+ import { RevealOnScroll } from "@/components/reveal-on-scroll" ;
1113
1214export const dynamic = "force-dynamic" ;
1315
@@ -17,10 +19,14 @@ export default async function DashboardPage() {
1719 const cookie = cookies ( ) . toString ( ) ;
1820 let user : User ;
1921 let carts : Cart [ ] ;
22+ let daily : DailyStat [ ] = [ ] ;
2023 try {
21- [ user , carts ] = await Promise . all ( [
24+ // Insights must never break the dashboard — fetch it alongside the
25+ // critical calls but swallow its failures into an empty series.
26+ [ user , carts , daily ] = await Promise . all ( [
2227 getCurrentUser ( { cookie } ) ,
2328 listMyCarts ( { cookie } ) ,
29+ getInsights ( { cookie } ) . catch ( ( ) => [ ] as DailyStat [ ] ) ,
2430 ] ) ;
2531 } catch {
2632 // 401 (or any auth failure) → kick to /login. The client-side NavBar
@@ -29,83 +35,46 @@ export default async function DashboardPage() {
2935 redirect ( "/login" ) ;
3036 }
3137
32- const totals = carts . reduce (
33- ( acc , c ) => ( {
34- views : acc . views + ( c . viewsLast7d || 0 ) ,
35- clicks : acc . clicks + ( c . clicksLast7d || 0 ) ,
36- } ) ,
37- { views : 0 , clicks : 0 } ,
38- ) ;
39-
4038 const firstName = user . displayName . split ( " " ) [ 0 ] || "creator" ;
4139 const isEmpty = carts . length === 0 ;
42- const productCount = carts . reduce ( ( n , c ) => n + ( c . products ?. length ?? 0 ) , 0 ) ;
40+
41+ // The featured "top cart" = highest 7-day views. TopCartCard hides itself
42+ // when that cart has no views yet.
43+ const topCart = isEmpty
44+ ? null
45+ : carts . reduce ( ( best , c ) =>
46+ ( c . viewsLast7d || 0 ) > ( best . viewsLast7d || 0 ) ? c : best ,
47+ ) ;
4348
4449 return (
45- < div className = "mx-auto max-w-6xl px-4 sm:px-6 py-10 pb-24 sm:pb-10" >
46- { /* GREETING */ }
47- < div className = "flex items-center gap-4 mb-8" >
48- { user . avatarUrl && (
49- < Image
50- src = { user . avatarUrl }
51- width = { 56 }
52- height = { 56 }
53- alt = { user . displayName }
54- unoptimized
55- className = "rounded-full border border-rule shrink-0"
56- />
57- ) }
58- < div >
59- < p className = "text-sm text-muted mb-0.5" >
60- { isEmpty ? "Welcome to shoplit" : "Welcome back" }
61- </ p >
62- < h1 className = "font-serif text-3xl sm:text-4xl tracking-tight leading-none" >
63- { isEmpty ? `Hi ${ firstName } 👋` : `Your carts, ${ firstName } ` }
64- </ h1 >
65- </ div >
66- </ div >
50+ < div className = "mx-auto max-w-6xl px-4 py-10 pb-24 sm:px-6 sm:pb-10" >
51+ < DashboardHero user = { user } />
6752
6853 < InstallNudge />
6954
7055 { isEmpty ? (
7156 < FirstTimeOnboarding firstName = { firstName } />
7257 ) : (
7358 < >
74- { /* STATS */ }
75- < div className = "grid grid-cols-3 gap-3 sm:gap-5 mb-12" >
76- < StatCard
77- icon = { < ShoppingBag size = { 18 } /> }
78- label = "Active carts"
79- value = { carts . length }
80- hint = { `${ productCount } ${ productCount === 1 ? "product" : "products" } curated` }
81- />
82- < StatCard
83- icon = { < Eye size = { 18 } /> }
84- label = "Views"
85- value = { totals . views }
86- hint = "across all carts · 7d"
87- />
88- < StatCard
89- icon = { < MousePointerClick size = { 18 } /> }
90- label = "Clicks"
91- value = { totals . clicks }
92- hint = "through to retailers · 7d"
93- />
59+ { /* INSIGHTS + TOP CART */ }
60+ < div className = "mb-12 grid gap-6 lg:grid-cols-2" >
61+ < InsightSummary daily = { daily } carts = { carts } />
62+ < TopCartCard cart = { topCart } />
9463 </ div >
9564
9665 { /* HEADER ROW */ }
97- < div className = "flex items-center justify-between gap-3 mb-6 " >
98- < h2 className = "font-serif text-2xl" > All carts</ h2 >
99- < div className = "flex items-center gap-2 shrink-0 " >
66+ < div className = "mb-6 flex items-center justify-between gap-3" >
67+ < h2 className = "font-serif text-2xl" > Your carts</ h2 >
68+ < div className = "flex shrink-0 items-center gap-2" >
10069 < Link
10170 href = "/add"
102- className = "inline-flex items-center gap-2 rounded-full bg-ink text-cream px-5 py-2.5 font-medium hover:opacity-90 transition-opacity"
71+ className = "inline-flex min-h-[44px] items-center gap-2 rounded-full bg-ink px-5 font-medium text-cream transition-opacity hover:opacity-90 "
10372 >
10473 < Plus size = { 16 } /> Add a product
10574 </ Link >
10675 < Link
10776 href = "/dashboard/carts/new"
108- className = "inline-flex items-center gap-2 rounded-full border border-ink px-4 py-2.5 font-medium hover:bg-paper transition-colors "
77+ className = "inline-flex min-h-[44px] items-center gap-2 rounded-full border border-ink px-4 font-medium transition-colors hover:bg-paper"
10978 >
11079 New cart
11180 </ Link >
@@ -114,57 +83,31 @@ export default async function DashboardPage() {
11483
11584 { /* GRID */ }
11685 < div className = "grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3" >
117- { carts . map ( ( c ) => (
118- < CartCard key = { c . id } cart = { c } />
86+ { carts . map ( ( c , i ) => (
87+ < RevealOnScroll key = { c . id } index = { i } >
88+ < CartCard cart = { c } />
89+ </ RevealOnScroll >
11990 ) ) }
120- < NewCartTile />
91+ < RevealOnScroll index = { carts . length } >
92+ < NewCartTile />
93+ </ RevealOnScroll >
12194 </ div >
12295 </ >
12396 ) }
12497 </ div >
12598 ) ;
12699}
127100
128- function StatCard ( {
129- icon,
130- label,
131- value,
132- hint,
133- } : {
134- icon : React . ReactNode ;
135- label : string ;
136- value : number ;
137- hint : string ;
138- } ) {
139- return (
140- < div className = "group rounded-2xl border border-rule bg-cream p-4 sm:p-6 transition-shadow hover:shadow-sm" >
141- < div className = "flex items-center justify-between mb-3 sm:mb-4" >
142- < span
143- className = "grid place-items-center size-9 sm:size-10 rounded-xl text-accent"
144- style = { { backgroundColor : "color-mix(in srgb, var(--accent) 12%, transparent)" } }
145- >
146- { icon }
147- </ span >
148- </ div >
149- < p className = "font-serif text-3xl sm:text-4xl tabular-nums leading-none mb-1.5" >
150- < AnimatedNumber value = { value } />
151- </ p >
152- < p className = "text-xs sm:text-sm font-medium" > { label } </ p >
153- < p className = "text-xs text-muted mt-0.5 hidden sm:block" > { hint } </ p >
154- </ div >
155- ) ;
156- }
157-
158101function NewCartTile ( ) {
159102 return (
160103 < Link
161104 href = "/dashboard/carts/new"
162- className = "group flex flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed border-rule text-muted hover:border-accent hover:text-accent transition-colors min-h-[220px] "
105+ className = "group flex min-h-[220px] flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed border-rule text-muted transition-colors hover:border-accent hover:text-accent"
163106 >
164- < span className = "grid place-items-center size-12 rounded-full border-2 border-current" >
107+ < span className = "grid size-12 place-items-center rounded-full border-2 border-current" >
165108 < Plus size = { 22 } />
166109 </ span >
167- < span className = "font-medium text-sm" > New cart</ span >
110+ < span className = "text-sm font-medium " > New cart</ span >
168111 < span className = "text-xs text-muted group-hover:text-accent/80" > Start curating</ span >
169112 </ Link >
170113 ) ;
@@ -173,28 +116,28 @@ function NewCartTile() {
173116function FirstTimeOnboarding ( { firstName } : { firstName : string } ) {
174117 return (
175118 < div className = "rounded-2xl border border-rule bg-paper p-6 sm:p-10" >
176- < div className = "flex items-start gap-3 mb-4 " >
177- < Sparkles size = { 20 } className = "text-accent shrink-0 mt-1 " />
119+ < div className = "mb-4 flex items-start gap-3" >
120+ < Sparkles size = { 20 } className = "mt-1 shrink-0 text-accent " />
178121 < div >
179- < h2 className = "font-serif text-2xl sm:text-3xl mb-2 " >
122+ < h2 className = "mb-2 font-serif text-2xl sm:text-3xl" >
180123 Let's build your first cart, { firstName }
181124 </ h2 >
182- < p className = "text-muted leading-relaxed max-w-xl " >
125+ < p className = "max-w-xl leading-relaxed text-muted " >
183126 Curate a few products you love into a single shareable link.
184127 Followers tap, you get the click. Free forever.
185128 </ p >
186129 </ div >
187130 </ div >
188131
189- < ol className = "grid gap-4 sm:grid-cols-3 mb-6 mt-8 " >
132+ < ol className = "mb-6 mt-8 grid gap-4 sm:grid-cols-3" >
190133 < Step n = { 1 } title = "Name your cart" body = "Something short — "Diwali Edit", "Desk Setup"." />
191134 < Step n = { 2 } title = "Paste product URLs" body = "Amazon, Myntra, Nykaa — we pull the title and image." />
192135 < Step n = { 3 } title = "Share the link" body = "One URL, beautifully laid out for your followers." />
193136 </ ol >
194137
195138 < Link
196139 href = "/dashboard/carts/new"
197- className = "inline-flex items-center gap-2 rounded-full bg-ink text-cream px-6 py-3 font-medium hover:opacity-90 transition-opacity"
140+ className = "inline-flex min-h-[44px] items-center gap-2 rounded-full bg-ink px-6 font-medium text-cream transition-opacity hover:opacity-90 "
198141 >
199142 < Plus size = { 16 } /> Create your first cart
200143 </ Link >
@@ -204,10 +147,10 @@ function FirstTimeOnboarding({ firstName }: { firstName: string }) {
204147
205148function Step ( { n, title, body } : { n : number ; title : string ; body : string } ) {
206149 return (
207- < li className = "rounded-lg bg-cream border border-rule p-4" >
208- < p className = "font-serif text-xl mb-1 text-accent" > { n } .</ p >
209- < p className = "font-medium text-sm mb-1 " > { title } </ p >
210- < p className = "text-xs text-muted leading-relaxed" > { body } </ p >
150+ < li className = "rounded-lg border border-rule bg-cream p-4" >
151+ < p className = "mb-1 font-serif text-xl text-accent" > { n } .</ p >
152+ < p className = "mb-1 text-sm font-medium " > { title } </ p >
153+ < p className = "text-xs leading-relaxed text-muted " > { body } </ p >
211154 </ li >
212155 ) ;
213156}
0 commit comments