You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Redesign the logged-in dashboard (`/dashboard`) around three pillars: a hero with your profile link + live preview, beautiful insights, and premium polish. Two tracks (Go backend, web frontend), disjoint files → parallelizable.
7
+
8
+
## Contract (normative)
9
+
New authed endpoint: `GET /api/v1/insights` → `{ "daily": DailyStat[] }`
10
+
-`DailyStat = { "date": "YYYY-MM-DD", "views": number, "clicks": number }`
11
+
- Exactly 14 entries, ascending by date, ending today (`today-13 … today`), **zero-filled** for days with no activity. Scope: all carts owned by the viewer.
12
+
13
+
Everything else the dashboard needs (per-cart `viewsLast7d/clicksLast7d/reachLast7d`, totals, top cart) is already in `listMyCarts()` — computed on the frontend. The daily series is the only new data.
14
+
15
+
---
16
+
17
+
## Workstream A — Backend (Go)
18
+
19
+
### sqlc queries (append to `internal/db/queries.sql`, regen with `"$(go env GOPATH)/bin/sqlc" generate`)
(`links.cart_id` is nullable; the JOIN naturally drops non-cart links.)
39
+
40
+
### Service (`internal/carts/service.go`)
41
+
-`AccountDailyStats(ctx, userID int64) ([]DailyStat, error)`: run both queries, merge by date into a **14-day, ascending, zero-filled** slice covering `today-13 … today` (build the date skeleton in Go using `time.Now().UTC()`, then fill from the query rows keyed by date). `DailyStat` is a small struct `{ Date time.Time / string, Views int64, Clicks int64 }`.
42
+
43
+
### Marshal (`internal/carts/marshal.go`)
44
+
-`DailyStatJSON { Date string `json:"date"`; Views int `json:"views"`; Clicks int `json:"clicks"` }` with `Date` formatted `"2006-01-02"`. A `MarshalDailyStats([]DailyStat) []DailyStatJSON` helper.
-`getInsights`: read `uid` from context, call `AccountDailyStats`, write `{"daily": MarshalDailyStats(...)}` (200). On error → 500.
49
+
50
+
### Tests
51
+
-`internal/carts/service_test.go` or `handlers_test.go`: seed `cart_views_daily` + `click_daily` across several days (incl. a gap day) for the user's cart(s); assert the series is length 14, ascending, zero-filled on the gap, correct sums, and excludes another user's carts. Handler test: `GET /api/v1/insights` returns a 14-element `daily` array.
-`web/lib/api-client.ts`: `getInsights(opts?: AuthOpts): Promise<DailyStat[]>` → `GET /api/v1/insights`, returns the `daily` array (`[]` on absence).
63
+
-`web/lib/insights.ts` (pure, tested): `summarizeDaily(daily: DailyStat[])` → `{ viewsThisWeek, viewsPrevWeek, viewsDeltaPct, clicksThisWeek, clicksPrevWeek, clicksDeltaPct }` (last 7 vs the prior 7; delta `null`/0-safe when prev week is 0). Unit-tested (node-env vitest).
64
+
65
+
### Components
66
+
-`web/components/sparkline.tsx` — pure inline **SVG** sparkline from `values: number[]` (props: values, width, height, className for stroke). No chart library. Renders nothing/flat baseline for all-zero input.
67
+
-`web/components/dashboard-hero.tsx` (`"use client"`) — props `{ user }`. Left: greeting (avatar + "Hi {firstName}" + warm line) and the **profile link**``{origin}/u/{handle}`` with **Copy** (reuse `lib/clipboard.ts`) + **Share** (reuse `share-sheet.tsx` or `navigator.share`). Right (lg+): a **live preview** — reuse `web/components/phone-frame.tsx` wrapping an `<iframe src={`/u/${handle}`} loading="lazy">` scaled to fit. Below `lg`: hide the phone, show a "View my profile →" link to `/u/{handle}`. Warm accent gradient background. Guard: if `handle` is empty, show the greeting without the link/preview.
68
+
-`web/components/insight-summary.tsx` — headline **views this week** via `AnimatedNumber`, a ▲/▼ **delta** chip vs last week (green up / muted down, from `summarizeDaily`), the `Sparkline` (14-day views), and a compact chip row: clicks (7d), reach (7d, summed from carts), carts count, products count. Server or client component (no interactivity needed beyond AnimatedNumber).
69
+
-`web/components/top-cart-card.tsx` — featured "⭐ Top cart" = the cart with the highest `viewsLast7d` (passed in). Larger cover, title, its views/clicks. Links to `/dashboard/carts/{id}`. Hidden if no carts have any views.
70
+
71
+
### Page (`web/app/dashboard/page.tsx`)
72
+
- Fetch `user`, `carts`, and `getInsights()` in parallel (forward cookie; insights failure → `[]`, never break the page).
- Empty state: keep `FirstTimeOnboarding` (a slimmed hero greeting is fine; skip insights/top-cart when there are no carts).
75
+
- Premium polish: warm gradient in hero, `RevealOnScroll` entrance on grid, existing hover-lift on cards, refined serif hierarchy. Keep within the existing theme tokens (ink/cream/paper/accent/muted/rule).
76
+
77
+
### Mobile
78
+
Hero stacks (phone preview hidden → "View my profile" link); insight chips wrap; sparkline is responsive width; grid stays `grid-cols-1 sm:grid-cols-2 lg:grid-cols-3`; ≥44px tap targets; keep `pb-24 sm:pb-10`.
79
+
80
+
### Tests
81
+
-`web/lib/insights.test.ts` (summarizeDaily: week split, delta %, zero-prev-week safety, short arrays).
0 commit comments