Skip to content

Commit 10ab25a

Browse files
committed
feat(web): wow dashboard — hero + insights + polish
Hero with profile link + live phone preview of /u/{handle}; insights (animated views-this-week, 14-day SVG sparkline, week delta, top-cart highlight); premium polish (gradient hero, scroll-reveal grid). Replaces the flat 3-stat block.
1 parent 84f5747 commit 10ab25a

10 files changed

Lines changed: 606 additions & 109 deletions

File tree

web/app/dashboard/page.tsx

Lines changed: 51 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import Link from "next/link";
2-
import Image from "next/image";
32
import { cookies } from "next/headers";
43
import { 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";
87
import { CartCard } from "@/components/cart-card";
9-
import { AnimatedNumber } from "@/components/animated-number";
108
import { 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

1214
export 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-
158101
function 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() {
173116
function 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&apos;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 — &quot;Diwali Edit&quot;, &quot;Desk Setup&quot;." />
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

205148
function 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

Comments
 (0)