Skip to content

Commit 3f99be3

Browse files
committed
Merge feature/feed-and-nav-ux: Your-feed activity stream + uniform nav
/dashboard/following becomes a single-column new-cart activity feed. A shared (public)/layout.tsx makes every public page auth-aware, fixing the 'Sign in while signed in' bug; the immersive /c/[slug] moves to a chrome-less group. Frontend-only.
2 parents 707e045 + a009890 commit 3f99be3

21 files changed

Lines changed: 384 additions & 121 deletions

File tree

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# "Your feed" activity stream + uniform navigation — Design
2+
3+
Date: 2026-05-25
4+
Status: Approved for implementation
5+
6+
Two independent, frontend-only workstreams (disjoint file sets → parallelizable).
7+
8+
---
9+
10+
## Workstream A — "Your feed" activity stream
11+
12+
### Goal
13+
Redesign `/dashboard/following` from a grid of cart cards into a single-column **activity stream of new-cart events** from creators you follow, newest-first.
14+
15+
### Why frontend-only
16+
`getFollowingFeed()` already returns `Cart[]` (newest-first by `createdAt`) with everything an entry needs: `ownerHandle`, `ownerDisplayName`, `ownerAvatarUrl`, `createdAt`, `title`, `slug`, `products[]`. No backend, query, or migration change.
17+
18+
### Pieces
19+
1. **`web/lib/relative-time.ts`** — pure `relativeTime(iso: string, now?: Date): string`:
20+
- `< 60s` → "just now"; `< 60m` → "Nm ago"; `< 24h` → "Nh ago"; `< 7d` → "Nd ago"; else short date "Mar 4" (and include year if a different year).
21+
- Pure + deterministic (accepts injectable `now`) so it is unit-testable in the repo's node-env vitest.
22+
- **`web/lib/relative-time.test.ts`** — cases for each bucket + boundary + older-than-a-week date formatting.
23+
24+
2. **`web/components/feed-item.tsx`** — presentational (no `"use client"` needed). Props: `cart: Cart`. Renders one entry:
25+
- Header: avatar + `@ownerHandle` linking to `/u/{ownerHandle}`, then `· {relativeTime(createdAt)}`.
26+
- Headline (muted, small): `shared a new cart`.
27+
- Cart title (`font-serif`) linking to `/c/{slug}`.
28+
- Thumbnail row: up to **4** product images (64px square, `rounded-lg`, `object-cover`, `unoptimized`); a neutral placeholder block when a product has no `imageUrl`; if `products.length > 4`, a trailing `+N` chip. Thumbnails link to `/c/{slug}` (the cart), not individual products.
29+
- Footer: `{products.length} products · View cart →` linking to `/c/{slug}`.
30+
- Use discrete links (creator vs cart) — do NOT wrap the whole card in one `<Link>`.
31+
- Card chrome: `rounded-2xl border border-rule bg-cream`, subtle hover shadow.
32+
33+
3. **`web/app/dashboard/following/page.tsx`** — rewrite the list section:
34+
- Centered single column: `mx-auto max-w-2xl ... space-y-4` (or `space-y-5`).
35+
- Keep the page heading; update copy to "Your feed" / subtitle "New carts from creators you follow."
36+
- Map feed → `<FeedItem cart={c} />`.
37+
- Keep & lightly refine the existing empty state (CTA → `/discover`).
38+
- Keep `force-dynamic`, the cookie-forwarding fetch, and the `/login` redirect on auth failure.
39+
40+
### Mobile
41+
Single column at all widths; thumbnails (4×64 + gaps) fit at 360px; tap targets ≥44px (creator link, View-cart CTA); keep bottom-nav padding (`pb-24 sm:pb-10`).
42+
43+
### Out of scope (v1)
44+
Tap-thumbnail-to-shop; unread/seen state; "items added to existing cart" events (only new-cart events selected); pagination UI.
45+
46+
### Files (A) — does NOT touch any nav component
47+
Create: `web/lib/relative-time.ts`, `web/lib/relative-time.test.ts`, `web/components/feed-item.tsx`.
48+
Modify: `web/app/dashboard/following/page.tsx`.
49+
50+
---
51+
52+
## Workstream B — Uniform navigation + auth-state fix
53+
54+
### Problems
55+
1. **"Sign in" shows while signed in** on `app/(public)/feedback/page.tsx` and `app/not-found.tsx` — both render a bare `<NavBar variant="marketing" />` with **no `user` prop**, so the nav can't reflect login state. (feedback is a `"use client"` page, so it could not render the async server `MarketingNav` directly — hence the bare NavBar.)
56+
2. **Inconsistent chrome** — each `(public)` page individually renders `<MarketingNav/>` + `<Footer/>`; there is no shared layout, so it drifts.
57+
58+
### Fix: a shared server layout for the public group
59+
1. **Create `web/app/(public)/layout.tsx`** (server component):
60+
```tsx
61+
import { MarketingNav } from "@/components/marketing-nav";
62+
import { Footer } from "@/components/footer";
63+
export default function PublicLayout({ children }: { children: React.ReactNode }) {
64+
return (
65+
<>
66+
<MarketingNav />
67+
<main className="min-h-[calc(100vh-15rem)]">{children}</main>
68+
<Footer />
69+
</>
70+
);
71+
}
72+
```
73+
`MarketingNav` is the auth-aware server wrapper (resolves the session cookie → passes `user`), so EVERY public page — including client pages like feedback — now gets a login-state-correct nav. This both fixes the bug and removes the drift.
74+
75+
2. **Strip per-page chrome** from all `(public)` pages so chrome isn't doubled. For each of: `discover`, `feedback`, `get-extension`, `legal/extension-privacy`, `legal/privacy`, `legal/terms`, `mobile`, `page` (home), `roadmap`, `u/[handle]` — remove their own `<MarketingNav/>` / `<NavBar .../>` and `<Footer/>` and the now-unused imports, plus any redundant wrapping `<>...</>` / outer `<main>` that the layout now supplies. Leave only page content. (Do NOT touch `dashboard/following` — different route group, owned by workstream A.)
76+
77+
3. **Keep the cart page chrome-less.** `/c/[slug]` is an intentional immersive share page (its own hero + footer, no top nav). The new `(public)/layout.tsx` would wrap it, so move it out of the group: relocate
78+
`app/(public)/c/[slug]/page.tsx``app/(share)/c/[slug]/page.tsx`
79+
`app/(public)/c/[slug]/loading.tsx``app/(share)/c/[slug]/loading.tsx`
80+
Route groups don't affect URLs, so the public URL stays `/c/{slug}`. The `(share)` group has **no** layout (inherits root only) → identical chrome-less rendering as today. Change nothing else in those files.
81+
82+
4. **Fix `app/not-found.tsx`** (a server component): replace bare `<NavBar variant="marketing" />` + `<Footer/>` with `<MarketingNav/>` + `<Footer/>` (import `MarketingNav` from `@/components/marketing-nav`). It is app-root (not under `(public)`), so it does not inherit the public layout and keeps rendering its own chrome — just auth-aware now.
83+
84+
5. **Relabel "Following" → "Feed"** to match product language:
85+
- `web/components/nav-bar.tsx` — the app-variant "Following" link text → "Feed".
86+
- `web/components/mobile-bottom-nav.tsx` — the "Following" item label → "Feed".
87+
- Keep the route `/dashboard/following` (no file/route rename).
88+
89+
### Deliberately left as-is
90+
`/login`, `/add`, `/connect-extension` are focused single-purpose flows (top-level routes, no chrome today). Leaving them chrome-light is intentional; not part of this pass.
91+
92+
### Uniformity result
93+
- All public pages: one identical auth-aware `MarketingNav` + `Footer` from the shared layout.
94+
- All dashboard pages: already share `dashboard/layout.tsx` (app nav + bottom nav + footer).
95+
- `/c/[slug]`: intentionally chrome-less, preserved.
96+
- The marketing↔app nav difference is by design (different contexts), but both are now auth-state-correct everywhere.
97+
98+
### Files (B) — does NOT touch workstream A's files
99+
Create: `web/app/(public)/layout.tsx`.
100+
Move: `web/app/(public)/c/[slug]/{page.tsx,loading.tsx}``web/app/(share)/c/[slug]/`.
101+
Modify: the 10 `(public)/*` pages listed (strip chrome), `web/app/not-found.tsx`, `web/components/nav-bar.tsx`, `web/components/mobile-bottom-nav.tsx`.
102+
103+
---
104+
105+
## Verification (both workstreams)
106+
Run from `web/`: `pnpm lint`, `pnpm exec tsc --noEmit`, `pnpm test`. (The integrating engineer runs the authoritative `pnpm build` + live route check — agents must NOT run `pnpm build` to avoid clobbering `.next` concurrently.)
107+
108+
Post-integration route checks (build + serve): every page renders exactly one nav + one footer; feedback & a 404 show "Dashboard" (not "Sign in") when logged in; `/c/{slug}` still renders with no top nav; `/dashboard/following` shows the activity stream.

web/app/(public)/discover/page.tsx

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import type { Metadata } from "next";
22
import { cookies } from "next/headers";
33
import type { Creator } from "@/lib/types";
44
import { listCreators } from "@/lib/api-client";
5-
import { MarketingNav } from "@/components/marketing-nav";
6-
import { Footer } from "@/components/footer";
75
import { CreatorCard } from "@/components/creator-card";
86

97
export const dynamic = "force-dynamic";
@@ -25,33 +23,29 @@ export default async function DiscoverPage() {
2523
}
2624

2725
return (
28-
<>
29-
<MarketingNav />
30-
<main className="mx-auto max-w-6xl px-4 sm:px-6 pb-24 sm:pb-16">
31-
<section className="pt-12 pb-8 sm:pt-16 sm:pb-10 max-w-2xl">
32-
<p className="text-sm text-accent uppercase tracking-widest font-medium mb-3">Discover</p>
33-
<h1 className="font-serif text-4xl sm:text-5xl leading-[1.05] mb-3">
34-
Find creators you&apos;ll love
35-
</h1>
36-
<p className="text-muted text-lg leading-relaxed">
37-
Browse the people curating on shoplit. Follow your favourites and their newest carts
38-
land in your feed.
39-
</p>
40-
</section>
26+
<div className="mx-auto max-w-6xl px-4 sm:px-6 pb-24 sm:pb-16">
27+
<section className="pt-12 pb-8 sm:pt-16 sm:pb-10 max-w-2xl">
28+
<p className="text-sm text-accent uppercase tracking-widest font-medium mb-3">Discover</p>
29+
<h1 className="font-serif text-4xl sm:text-5xl leading-[1.05] mb-3">
30+
Find creators you&apos;ll love
31+
</h1>
32+
<p className="text-muted text-lg leading-relaxed">
33+
Browse the people curating on shoplit. Follow your favourites and their newest carts
34+
land in your feed.
35+
</p>
36+
</section>
4137

42-
{creators.length === 0 ? (
43-
<p className="text-muted py-16 text-center">
44-
No creators to show yet — check back soon.
45-
</p>
46-
) : (
47-
<div className="grid grid-cols-2 gap-3 sm:gap-5 lg:grid-cols-3">
48-
{creators.map((c) => (
49-
<CreatorCard key={c.handle} creator={c} />
50-
))}
51-
</div>
52-
)}
53-
</main>
54-
<Footer />
55-
</>
38+
{creators.length === 0 ? (
39+
<p className="text-muted py-16 text-center">
40+
No creators to show yet — check back soon.
41+
</p>
42+
) : (
43+
<div className="grid grid-cols-2 gap-3 sm:gap-5 lg:grid-cols-3">
44+
{creators.map((c) => (
45+
<CreatorCard key={c.handle} creator={c} />
46+
))}
47+
</div>
48+
)}
49+
</div>
5650
);
5751
}

web/app/(public)/feedback/page.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import { useState } from "react";
44
import Link from "next/link";
5-
import { NavBar } from "@/components/nav-bar";
6-
import { Footer } from "@/components/footer";
75
import { submitFeedback } from "@/lib/api-client";
86
import { Lightbulb, ArrowRight } from "lucide-react";
97

@@ -39,10 +37,8 @@ export default function FeedbackPage() {
3937
};
4038

4139
return (
42-
<>
43-
<NavBar variant="marketing" />
44-
<main className="mx-auto max-w-6xl px-4 sm:px-6">
45-
{/* HERO */}
40+
<div className="mx-auto max-w-6xl px-4 sm:px-6">
41+
{/* HERO */}
4642
<section className="pt-16 pb-10 sm:pt-24 sm:pb-14 text-center max-w-2xl mx-auto">
4743
<span
4844
className="inline-grid place-items-center size-12 rounded-2xl text-accent mb-5"
@@ -164,8 +160,6 @@ export default function FeedbackPage() {
164160
)}
165161
</div>
166162
</section>
167-
</main>
168-
<Footer />
169-
</>
163+
</div>
170164
);
171165
}

web/app/(public)/get-extension/page.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import type { Metadata } from "next";
22
import Link from "next/link";
3-
import { MarketingNav } from "@/components/marketing-nav";
4-
import { Footer } from "@/components/footer";
53
import { Puzzle, Monitor, Smartphone } from "lucide-react";
64

75
const STORE_URL = "https://chromewebstore.google.com/detail/shoplit-%E2%80%94-add-to-cart/dplbbiamddaaimhjennfncbpbnkfconn";
@@ -26,10 +24,8 @@ const mobileSteps = [
2624

2725
export default function GetExtensionPage() {
2826
return (
29-
<>
30-
<MarketingNav />
31-
<main className="mx-auto max-w-2xl px-4 sm:px-6 py-16">
32-
<div className="text-center mb-10">
27+
<div className="mx-auto max-w-2xl px-4 sm:px-6 py-16">
28+
<div className="text-center mb-10">
3329
<span
3430
className="inline-grid place-items-center size-14 rounded-2xl text-accent mb-4"
3531
style={{ backgroundColor: "color-mix(in srgb, var(--accent) 12%, transparent)" }}
@@ -117,8 +113,6 @@ export default function GetExtensionPage() {
117113
See the full mobile guide →
118114
</Link>
119115
</p>
120-
</main>
121-
<Footer />
122-
</>
116+
</div>
123117
);
124118
}

web/app/(public)/layout.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { MarketingNav } from "@/components/marketing-nav";
2+
import { Footer } from "@/components/footer";
3+
4+
export default function PublicLayout({ children }: { children: React.ReactNode }) {
5+
return (
6+
<>
7+
<MarketingNav />
8+
<main className="min-h-[calc(100vh-15rem)]">{children}</main>
9+
<Footer />
10+
</>
11+
);
12+
}

web/app/(public)/legal/extension-privacy/page.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
import { MarketingNav } from "@/components/marketing-nav";
2-
import { Footer } from "@/components/footer";
3-
41
export const metadata = { title: "Extension Privacy · shoplit" };
52

63
export default function ExtensionPrivacyPage() {
74
return (
8-
<>
9-
<MarketingNav />
10-
<main className="mx-auto max-w-2xl px-4 sm:px-6 py-16">
11-
<h1 className="font-serif text-4xl mb-2">shoplit browser extension — Privacy</h1>
5+
<div className="mx-auto max-w-2xl px-4 sm:px-6 py-16">
6+
<h1 className="font-serif text-4xl mb-2">shoplit browser extension — Privacy</h1>
127
<p className="text-sm text-muted mb-8">Last updated: 2026-05-24</p>
138

149
<h2 className="font-serif text-2xl mt-8 mb-2">What the extension does</h2>
@@ -63,8 +58,6 @@ export default function ExtensionPrivacyPage() {
6358
<p className="leading-relaxed text-ink">
6459
Questions about this policy: <a href="mailto:mayur.das4@gmail.com" className="text-accent underline underline-offset-2">mayur.das4@gmail.com</a>.
6560
</p>
66-
</main>
67-
<Footer />
68-
</>
61+
</div>
6962
);
7063
}

web/app/(public)/legal/privacy/page.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
import { MarketingNav } from "@/components/marketing-nav";
2-
import { Footer } from "@/components/footer";
3-
41
export const metadata = { title: "Privacy · shoplit" };
52

63
export default function PrivacyPage() {
74
return (
8-
<>
9-
<MarketingNav />
10-
<main className="mx-auto max-w-2xl px-4 sm:px-6 py-16">
11-
<h1 className="font-serif text-4xl mb-2">Privacy</h1>
5+
<div className="mx-auto max-w-2xl px-4 sm:px-6 py-16">
6+
<h1 className="font-serif text-4xl mb-2">Privacy</h1>
127
<p className="text-sm text-muted mb-8">Last updated: 2026-05-23</p>
138

149
<h2 className="font-serif text-2xl mt-8 mb-2">What we collect</h2>
@@ -44,8 +39,6 @@ export default function PrivacyPage() {
4439
<p className="leading-relaxed text-ink">
4540
Questions? Reach us via the GitHub repo&apos;s issue tracker.
4641
</p>
47-
</main>
48-
<Footer />
49-
</>
42+
</div>
5043
);
5144
}

web/app/(public)/legal/terms/page.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
import { MarketingNav } from "@/components/marketing-nav";
2-
import { Footer } from "@/components/footer";
3-
41
export const metadata = { title: "Terms · shoplit" };
52

63
export default function TermsPage() {
74
return (
8-
<>
9-
<MarketingNav />
10-
<main className="mx-auto max-w-2xl px-4 sm:px-6 py-16">
11-
<h1 className="font-serif text-4xl mb-2">Terms</h1>
5+
<div className="mx-auto max-w-2xl px-4 sm:px-6 py-16">
6+
<h1 className="font-serif text-4xl mb-2">Terms</h1>
127
<p className="text-sm text-muted mb-8">Last updated: 2026-05-23</p>
138

149
<h2 className="font-serif text-2xl mt-8 mb-2">Using shoplit</h2>
@@ -51,8 +46,6 @@ export default function TermsPage() {
5146
<p className="leading-relaxed text-ink">
5247
Questions or disputes? Reach us via the GitHub repo&apos;s issue tracker.
5348
</p>
54-
</main>
55-
<Footer />
56-
</>
49+
</div>
5750
);
5851
}

web/app/(public)/mobile/page.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import type { Metadata } from "next";
22
import Link from "next/link";
3-
import { MarketingNav } from "@/components/marketing-nav";
4-
import { Footer } from "@/components/footer";
53
import { Smartphone, Apple, Share2, ImageIcon } from "lucide-react";
64

75
export const metadata: Metadata = {
@@ -25,10 +23,8 @@ const iosSteps = [
2523

2624
export default function MobileGuidePage() {
2725
return (
28-
<>
29-
<MarketingNav />
30-
<main className="mx-auto max-w-2xl px-4 sm:px-6 py-16">
31-
{/* HERO */}
26+
<div className="mx-auto max-w-2xl px-4 sm:px-6 py-16">
27+
{/* HERO */}
3228
<div className="text-center mb-10">
3329
<span
3430
className="inline-grid place-items-center size-14 rounded-2xl text-accent mb-4"
@@ -107,8 +103,6 @@ export default function MobileGuidePage() {
107103
No account yet? Create one free
108104
</Link>
109105
</div>
110-
</main>
111-
<Footer />
112-
</>
106+
</div>
113107
);
114108
}

web/app/(public)/page.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import Link from "next/link";
22
import { ArrowRight, Sparkles } from "lucide-react";
3-
import { MarketingNav } from "@/components/marketing-nav";
4-
import { Footer } from "@/components/footer";
53
import { CartCard } from "@/components/cart-card";
64
import { CartCover } from "@/components/cart-cover";
75
import { RevealOnScroll } from "@/components/reveal-on-scroll";
@@ -21,9 +19,7 @@ export default async function LandingPage() {
2119
}
2220
return (
2321
<>
24-
<MarketingNav />
25-
<main>
26-
{/* HERO — animated gradient background + cascading phone mockups */}
22+
{/* HERO — animated gradient background + cascading phone mockups */}
2723
<section className="relative overflow-hidden">
2824
{/* Soft warm gradient blobs (CSS-animated) behind the hero */}
2925
<div aria-hidden className="absolute inset-0 -z-10 pointer-events-none">
@@ -209,8 +205,6 @@ export default async function LandingPage() {
209205
</p>
210206
</div>
211207
</section>
212-
</main>
213-
<Footer />
214208
</>
215209
);
216210
}

0 commit comments

Comments
 (0)