478 lines
20 KiB
Markdown
478 lines
20 KiB
Markdown
---
|
|
phase: 26-discovery-landing-page
|
|
plan: 03
|
|
type: execute
|
|
wave: 3
|
|
depends_on: [26-01, 26-02]
|
|
files_modified:
|
|
- src/client/routes/index.tsx
|
|
- src/client/components/PublicSetupCard.tsx
|
|
- src/client/routes/users/$userId.tsx
|
|
autonomous: false
|
|
requirements: [DISC-01, DISC-02, DISC-03, DISC-04, DISC-05]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Root URL shows a hero section with a catalog search bar"
|
|
- "Clicking the search bar opens CatalogSearchOverlay"
|
|
- "Popular setups section shows setup cards with item counts"
|
|
- "Recently added items section shows GlobalItemCard components"
|
|
- "Trending categories section shows category names with item counts"
|
|
- "Authenticated users see Go to Collection link in hero"
|
|
- "Anonymous users see the page without login redirect"
|
|
artifacts:
|
|
- path: "src/client/routes/index.tsx"
|
|
provides: "Landing page with hero, popular setups, recent items, trending categories"
|
|
min_lines: 80
|
|
- path: "src/client/components/PublicSetupCard.tsx"
|
|
provides: "Enhanced setup card with optional itemCount and creatorName"
|
|
contains: "itemCount"
|
|
key_links:
|
|
- from: "src/client/routes/index.tsx"
|
|
to: "src/client/hooks/useDiscovery.ts"
|
|
via: "imports useDiscoverySetups, useDiscoveryItems, useDiscoveryCategories"
|
|
pattern: "from.*useDiscovery"
|
|
- from: "src/client/routes/index.tsx"
|
|
to: "src/client/stores/uiStore.ts"
|
|
via: "openCatalogSearch trigger from hero"
|
|
pattern: "openCatalogSearch"
|
|
- from: "src/client/routes/index.tsx"
|
|
to: "src/client/hooks/useAuth.ts"
|
|
via: "auth state for conditional Go to Collection CTA"
|
|
pattern: "useAuth"
|
|
- from: "src/client/routes/index.tsx"
|
|
to: "src/client/components/GlobalItemCard.tsx"
|
|
via: "renders catalog items in recent items section"
|
|
pattern: "GlobalItemCard"
|
|
- from: "src/client/routes/index.tsx"
|
|
to: "src/client/components/PublicSetupCard.tsx"
|
|
via: "renders setup cards in popular setups section"
|
|
pattern: "PublicSetupCard"
|
|
---
|
|
|
|
<objective>
|
|
Rewrite the landing page at `/` from a personal dashboard to a public discovery page. Enhance PublicSetupCard with itemCount and creatorName. Build hero section with search trigger, three content feed sections, and conditional auth CTA.
|
|
|
|
Purpose: This is the user-facing deliverable — the page visitors see first. Composes existing components with new discovery hooks into the layout specified by D-01 through D-11.
|
|
Output: Complete landing page at `/`, enhanced PublicSetupCard.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/26-discovery-landing-page/26-CONTEXT.md
|
|
@.planning/phases/26-discovery-landing-page/26-RESEARCH.md
|
|
@.planning/phases/26-discovery-landing-page/26-01-SUMMARY.md
|
|
|
|
<interfaces>
|
|
<!-- From plan 02: client hooks -->
|
|
From src/client/hooks/useDiscovery.ts:
|
|
```typescript
|
|
export interface DiscoverySetup {
|
|
id: number;
|
|
name: string;
|
|
createdAt: string;
|
|
itemCount: number;
|
|
creatorName: string | null;
|
|
}
|
|
export interface DiscoveryCategory {
|
|
name: string;
|
|
itemCount: number;
|
|
}
|
|
export function useDiscoverySetups(limit?: number): UseQueryResult
|
|
export function useDiscoveryItems(limit?: number): UseQueryResult
|
|
export function useDiscoveryCategories(limit?: number): UseQueryResult
|
|
```
|
|
|
|
From src/client/hooks/useAuth.ts:
|
|
```typescript
|
|
interface AuthState {
|
|
user: { id: string; email?: string } | null;
|
|
authenticated: boolean;
|
|
}
|
|
export function useAuth(): UseQueryResult<AuthState>
|
|
```
|
|
|
|
From src/client/stores/uiStore.ts:
|
|
```typescript
|
|
openCatalogSearch: (mode: "collection" | "thread") => void;
|
|
// Access via: const openCatalogSearch = useUIStore((s) => s.openCatalogSearch);
|
|
```
|
|
|
|
From src/client/components/GlobalItemCard.tsx:
|
|
```typescript
|
|
interface GlobalItemCardProps {
|
|
id: number; brand: string; model: string; category: string | null;
|
|
weightGrams: number | null; priceCents: number | null; imageUrl: string | null;
|
|
}
|
|
export function GlobalItemCard(props: GlobalItemCardProps): JSX.Element
|
|
```
|
|
|
|
From src/client/components/PublicSetupCard.tsx (current — will be enhanced):
|
|
```typescript
|
|
interface PublicSetupCardProps {
|
|
setup: { id: number; name: string; createdAt: string; };
|
|
}
|
|
export function PublicSetupCard({ setup }: PublicSetupCardProps): JSX.Element
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Enhance PublicSetupCard with itemCount and creatorName</name>
|
|
<files>src/client/components/PublicSetupCard.tsx, src/client/routes/users/$userId.tsx</files>
|
|
<read_first>
|
|
- src/client/components/PublicSetupCard.tsx (current component — full content)
|
|
- src/client/routes/users/$userId.tsx (existing usage of PublicSetupCard — check what shape is passed)
|
|
</read_first>
|
|
<action>
|
|
**Modify `src/client/components/PublicSetupCard.tsx`:**
|
|
|
|
Update the `PublicSetupCardProps` interface to add optional fields (per Pitfall 3 — keep optional to avoid breaking existing usages):
|
|
```typescript
|
|
interface PublicSetupCardProps {
|
|
setup: {
|
|
id: number;
|
|
name: string;
|
|
createdAt: string;
|
|
itemCount?: number; // NEW — optional for backward compat
|
|
creatorName?: string | null; // NEW — optional
|
|
};
|
|
}
|
|
```
|
|
|
|
Update the component JSX to display the new fields when present:
|
|
|
|
After the existing `<h3>` (setup name) and before/replacing the `<p>` date line, render:
|
|
- If `setup.creatorName` is truthy: `<p className="text-xs text-gray-400 mt-1">by {setup.creatorName}</p>`
|
|
- If `setup.itemCount` is defined and > 0: `<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-400">{setup.itemCount} items</span>`
|
|
- Keep the existing date display
|
|
|
|
Layout for the bottom area of the card (below the name):
|
|
```tsx
|
|
<div className="flex items-center justify-between mt-2">
|
|
<div className="flex items-center gap-2">
|
|
{setup.itemCount != null && setup.itemCount > 0 && (
|
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-400">
|
|
{setup.itemCount} {setup.itemCount === 1 ? "item" : "items"}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-gray-400">{formattedDate}</p>
|
|
</div>
|
|
{setup.creatorName && (
|
|
<p className="text-xs text-gray-400 mt-1">by {setup.creatorName}</p>
|
|
)}
|
|
```
|
|
|
|
Add `cursor-pointer` to the Link className (folded todo from CONTEXT.md).
|
|
|
|
**Verify `src/client/routes/users/$userId.tsx`:**
|
|
Read the file to confirm the existing `PublicSetupCard` usage still compiles. Since `itemCount` and `creatorName` are optional, the existing usage passing `{ id, name, createdAt }` will continue to work without changes. No modification needed to this file unless it already passes extra props.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun run build 2>&1 | tail -20</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- src/client/components/PublicSetupCard.tsx contains `itemCount?: number`
|
|
- src/client/components/PublicSetupCard.tsx contains `creatorName?: string | null`
|
|
- src/client/components/PublicSetupCard.tsx contains `setup.itemCount` (conditional rendering)
|
|
- src/client/components/PublicSetupCard.tsx contains `setup.creatorName` (conditional rendering)
|
|
- src/client/components/PublicSetupCard.tsx contains `cursor-pointer`
|
|
- `bun run build` succeeds without TypeScript errors
|
|
</acceptance_criteria>
|
|
<done>PublicSetupCard renders item count and creator name when provided, existing usages compile without changes, cursor-pointer applied.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Rewrite index.tsx as discovery landing page</name>
|
|
<files>src/client/routes/index.tsx</files>
|
|
<read_first>
|
|
- src/client/routes/index.tsx (current dashboard — will be completely rewritten)
|
|
- src/client/components/GlobalItemCard.tsx (props interface for rendering items)
|
|
- src/client/components/PublicSetupCard.tsx (enhanced props from Task 1)
|
|
- src/client/hooks/useDiscovery.ts (hook signatures from plan 02)
|
|
- src/client/hooks/useAuth.ts (useAuth hook pattern)
|
|
- src/client/stores/uiStore.ts (openCatalogSearch usage pattern)
|
|
- src/client/routes/__root.tsx (isDashboard detection — do NOT modify per Pitfall 2)
|
|
</read_first>
|
|
<action>
|
|
**Completely rewrite `src/client/routes/index.tsx`** (per D-03, retire DashboardPage entirely):
|
|
|
|
Remove ALL existing imports (DashboardCard, useFormatters, useSetups, useThreads, useTotals).
|
|
|
|
New imports:
|
|
```typescript
|
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
|
import { Search } from "lucide-react";
|
|
import { GlobalItemCard } from "../components/GlobalItemCard";
|
|
import { PublicSetupCard } from "../components/PublicSetupCard";
|
|
import { useAuth } from "../hooks/useAuth";
|
|
import { useDiscoverySetups, useDiscoveryItems, useDiscoveryCategories } from "../hooks/useDiscovery";
|
|
import { useUIStore } from "../stores/uiStore";
|
|
```
|
|
|
|
Note: Use `lucide-react` for the Search icon. The project already uses lucide-react (check existing imports). If the project uses the custom `LucideIcon` component instead, use `<LucideIcon name="search" className="w-5 h-5 text-gray-400" />` from `../lib/iconData`.
|
|
|
|
**Route export** (same file route, new component):
|
|
```typescript
|
|
export const Route = createFileRoute("/")({
|
|
component: LandingPage,
|
|
});
|
|
```
|
|
|
|
**LandingPage function** (per D-01, D-02):
|
|
```typescript
|
|
function LandingPage() {
|
|
const { data: auth } = useAuth();
|
|
const isAuthenticated = !!auth?.user;
|
|
const openCatalogSearch = useUIStore((s) => s.openCatalogSearch);
|
|
|
|
return (
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
<HeroSection isAuthenticated={isAuthenticated} onSearchFocus={() => openCatalogSearch("collection")} />
|
|
<PopularSetupsSection />
|
|
<RecentItemsSection />
|
|
<TrendingCategoriesSection />
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
**HeroSection** (per D-01, D-04, D-10, D-11):
|
|
```typescript
|
|
function HeroSection({ isAuthenticated, onSearchFocus }: { isAuthenticated: boolean; onSearchFocus: () => void }) {
|
|
return (
|
|
<div className="text-center mb-16">
|
|
<h1 className="text-3xl sm:text-4xl font-bold text-gray-900 mb-2">Discover Gear</h1>
|
|
<p className="text-gray-500 mb-8">Browse what other people carry</p>
|
|
<div
|
|
onClick={onSearchFocus}
|
|
onKeyDown={(e) => e.key === "Enter" && onSearchFocus()}
|
|
role="button"
|
|
tabIndex={0}
|
|
className="max-w-xl mx-auto flex items-center gap-3 px-4 py-3 bg-white rounded-xl border border-gray-200 hover:border-gray-300 cursor-pointer shadow-sm transition-all"
|
|
>
|
|
<Search className="w-5 h-5 text-gray-400 shrink-0" />
|
|
<span className="text-sm text-gray-400 flex-1 text-left">Search the catalog...</span>
|
|
</div>
|
|
{isAuthenticated && (
|
|
<div className="mt-4">
|
|
<Link to="/collection" className="text-sm text-gray-600 hover:text-gray-900 underline underline-offset-2 cursor-pointer">
|
|
Go to Collection
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
**PopularSetupsSection** (per D-02, D-05):
|
|
```typescript
|
|
function PopularSetupsSection() {
|
|
const { data, isLoading } = useDiscoverySetups(6);
|
|
const setups = data?.items ?? [];
|
|
|
|
if (!isLoading && setups.length === 0) return null;
|
|
|
|
return (
|
|
<section className="mb-12">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-semibold text-gray-900">Popular Setups</h2>
|
|
</div>
|
|
{isLoading ? (
|
|
<SectionSkeleton count={6} aspect="none" />
|
|
) : (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{setups.map((setup) => (
|
|
<PublicSetupCard key={setup.id} setup={setup} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|
|
```
|
|
|
|
**RecentItemsSection** (per D-02, D-06):
|
|
```typescript
|
|
function RecentItemsSection() {
|
|
const { data, isLoading } = useDiscoveryItems(8);
|
|
const items = data?.items ?? [];
|
|
|
|
if (!isLoading && items.length === 0) return null;
|
|
|
|
return (
|
|
<section className="mb-12">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-semibold text-gray-900">Recently Added</h2>
|
|
</div>
|
|
{isLoading ? (
|
|
<SectionSkeleton count={8} aspect="[4/3]" />
|
|
) : (
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4">
|
|
{items.map((item) => (
|
|
<GlobalItemCard
|
|
key={item.id}
|
|
id={item.id}
|
|
brand={item.brand}
|
|
model={item.model}
|
|
category={item.category}
|
|
weightGrams={item.weightGrams}
|
|
priceCents={item.priceCents}
|
|
imageUrl={item.imageUrl}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|
|
```
|
|
|
|
**TrendingCategoriesSection** (per D-02, D-07):
|
|
```typescript
|
|
function TrendingCategoriesSection() {
|
|
const { data, isLoading } = useDiscoveryCategories(12);
|
|
const categories = data ?? [];
|
|
|
|
if (!isLoading && categories.length === 0) return null;
|
|
|
|
return (
|
|
<section className="mb-12">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-semibold text-gray-900">Trending Categories</h2>
|
|
</div>
|
|
{isLoading ? (
|
|
<div className="flex flex-wrap gap-2">
|
|
{Array.from({ length: 8 }).map((_, i) => (
|
|
<div key={i} className="h-8 w-24 bg-gray-100 rounded-full animate-pulse" />
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-wrap gap-2">
|
|
{categories.map((cat) => (
|
|
<span
|
|
key={cat.name}
|
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium bg-gray-50 text-gray-700 border border-gray-100 hover:border-gray-200 hover:bg-gray-100 transition-all cursor-pointer"
|
|
>
|
|
{cat.name}
|
|
<span className="text-xs text-gray-400">{cat.itemCount}</span>
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|
|
```
|
|
|
|
**SectionSkeleton** helper (matches CatalogSearchOverlay animate-pulse pattern):
|
|
```typescript
|
|
function SectionSkeleton({ count, aspect }: { count: number; aspect: string }) {
|
|
return (
|
|
<div className={`grid ${aspect === "none" ? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3" : "grid-cols-2 sm:grid-cols-3 lg:grid-cols-4"} gap-4`}>
|
|
{Array.from({ length: count }).map((_, i) => (
|
|
<div key={i} className="bg-white rounded-xl border border-gray-100 overflow-hidden animate-pulse">
|
|
{aspect !== "none" && <div className={`aspect-${aspect} bg-gray-100`} />}
|
|
<div className="p-4 space-y-2">
|
|
<div className="h-3 bg-gray-100 rounded w-16" />
|
|
<div className="h-4 bg-gray-100 rounded w-32" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
Ensure all clickable elements have `cursor-pointer` (folded todo from CONTEXT.md).
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun run build 2>&1 | tail -20</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- src/client/routes/index.tsx does NOT contain `DashboardPage` or `DashboardCard` or `useTotals` (per D-03)
|
|
- src/client/routes/index.tsx contains `function LandingPage()`
|
|
- src/client/routes/index.tsx contains `function HeroSection(`
|
|
- src/client/routes/index.tsx contains `openCatalogSearch("collection")` (per D-04)
|
|
- src/client/routes/index.tsx contains `useDiscoverySetups` and `useDiscoveryItems` and `useDiscoveryCategories`
|
|
- src/client/routes/index.tsx contains `"Go to Collection"` (per D-10)
|
|
- src/client/routes/index.tsx contains `!!auth?.user` or `auth?.user` for auth check (per anti-pattern: do not use auth?.authenticated)
|
|
- src/client/routes/index.tsx contains `GlobalItemCard` import
|
|
- src/client/routes/index.tsx contains `PublicSetupCard` import
|
|
- src/client/routes/index.tsx contains `cursor-pointer` on the search bar div
|
|
- src/client/routes/index.tsx contains `animate-pulse` (loading skeletons)
|
|
- `bun run build` succeeds without errors
|
|
</acceptance_criteria>
|
|
<done>Landing page renders hero with search trigger, three feed sections with loading skeletons and empty-state handling, authenticated CTA, and all clickable elements have cursor-pointer. Build succeeds.</done>
|
|
</task>
|
|
|
|
<task type="checkpoint:human-verify" gate="blocking">
|
|
<name>Task 3: Visual verification of discovery landing page</name>
|
|
<files>src/client/routes/index.tsx</files>
|
|
<action>
|
|
No code changes — this is a visual verification checkpoint. The executor should start the dev server with `bun run dev` and present the verification steps to the user.
|
|
</action>
|
|
<what-built>
|
|
Complete discovery landing page replacing the personal dashboard at /. Features:
|
|
- Hero section with "Discover Gear" heading and catalog search bar trigger
|
|
- Popular Setups section with enhanced cards (item count, creator name)
|
|
- Recently Added Items section with GlobalItemCard components
|
|
- Trending Categories section with category pills
|
|
- Conditional "Go to Collection" link for authenticated users
|
|
- Loading skeletons for all sections
|
|
- Empty state handling (sections hide when no data)
|
|
</what-built>
|
|
<how-to-verify>
|
|
1. Run `bun run dev` and open http://localhost:5173/ in a browser
|
|
2. Verify the hero section shows "Discover Gear" heading with search bar
|
|
3. Click the search bar — it should open the full CatalogSearchOverlay
|
|
4. Verify sections appear below: Popular Setups, Recently Added, Trending Categories
|
|
5. If there is seed data, verify cards show correct information (item counts, creator names, images)
|
|
6. If no data exists, verify empty sections are hidden gracefully (no broken/empty grids)
|
|
7. Log in and verify "Go to Collection" link appears in hero area
|
|
8. Click "Go to Collection" — should navigate to /collection
|
|
9. Check responsive behavior: resize browser to mobile width, verify single-column layout
|
|
10. Verify all clickable elements show pointer cursor on hover
|
|
</how-to-verify>
|
|
<verify>
|
|
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun run build 2>&1 | tail -5</automated>
|
|
</verify>
|
|
<done>User has visually verified the landing page renders correctly with all sections, search trigger works, auth CTA appears for logged-in users, and responsive layout is correct.</done>
|
|
<resume-signal>Type "approved" or describe issues to fix</resume-signal>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `bun run build` — builds without errors
|
|
- Visual inspection of landing page at localhost:5173
|
|
- CatalogSearchOverlay opens from hero search bar
|
|
- Authenticated user sees "Go to Collection" link
|
|
- Anonymous user sees content immediately
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- Root URL shows discovery landing page (not personal dashboard)
|
|
- Hero search bar triggers CatalogSearchOverlay on click
|
|
- Three content sections render with real data or hide when empty
|
|
- PublicSetupCard displays item count and creator name
|
|
- Authenticated users see "Go to Collection" CTA in hero
|
|
- All clickable elements have cursor-pointer
|
|
- Build succeeds
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/26-discovery-landing-page/26-03-SUMMARY.md`
|
|
</output>
|