20 KiB
20 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 26-discovery-landing-page | 03 | execute | 3 |
|
|
false |
|
|
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.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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 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:
interface AuthState {
user: { id: string; email?: string } | null;
authenticated: boolean;
}
export function useAuth(): UseQueryResult<AuthState>
From src/client/stores/uiStore.ts:
openCatalogSearch: (mode: "collection" | "thread") => void;
// Access via: const openCatalogSearch = useUIStore((s) => s.openCatalogSearch);
From src/client/components/GlobalItemCard.tsx:
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):
interface PublicSetupCardProps {
setup: { id: number; name: string; createdAt: string; };
}
export function PublicSetupCard({ setup }: PublicSetupCardProps): JSX.Element
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.
cd /home/jean-luc-makiola/Development/projects/GearBox && bun run build 2>&1 | tail -20
- 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
PublicSetupCard renders item count and creator name when provided, existing usages compile without changes, cursor-pointer applied.
Task 2: Rewrite index.tsx as discovery landing page
src/client/routes/index.tsx
- 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)
**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).
cd /home/jean-luc-makiola/Development/projects/GearBox && bun run build 2>&1 | tail -20
- 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
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.
Task 3: Visual verification of discovery landing page
src/client/routes/index.tsx
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.
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)
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
cd /home/jean-luc-makiola/Development/projects/GearBox && bun run build 2>&1 | tail -5
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.
Type "approved" or describe issues to fix
- `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
<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>