Files
GearBox/.planning/phases/26-discovery-landing-page/26-03-PLAN.md

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
26-01
26-02
src/client/routes/index.tsx
src/client/components/PublicSetupCard.tsx
src/client/routes/users/$userId.tsx
false
DISC-01
DISC-02
DISC-03
DISC-04
DISC-05
truths artifacts key_links
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
path provides min_lines
src/client/routes/index.tsx Landing page with hero, popular setups, recent items, trending categories 80
path provides contains
src/client/components/PublicSetupCard.tsx Enhanced setup card with optional itemCount and creatorName itemCount
from to via pattern
src/client/routes/index.tsx src/client/hooks/useDiscovery.ts imports useDiscoverySetups, useDiscoveryItems, useDiscoveryCategories from.*useDiscovery
from to via pattern
src/client/routes/index.tsx src/client/stores/uiStore.ts openCatalogSearch trigger from hero openCatalogSearch
from to via pattern
src/client/routes/index.tsx src/client/hooks/useAuth.ts auth state for conditional Go to Collection CTA useAuth
from to via pattern
src/client/routes/index.tsx src/client/components/GlobalItemCard.tsx renders catalog items in recent items section GlobalItemCard
from to via pattern
src/client/routes/index.tsx src/client/components/PublicSetupCard.tsx renders setup cards in popular setups section PublicSetupCard
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.

<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
Task 1: Enhance PublicSetupCard with itemCount and creatorName src/client/components/PublicSetupCard.tsx, src/client/routes/users/$userId.tsx - src/client/components/PublicSetupCard.tsx (current component — full content) - src/client/routes/users/$userId.tsx (existing usage of PublicSetupCard — check what shape is passed) **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.
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>
After completion, create `.planning/phases/26-discovery-landing-page/26-03-SUMMARY.md`