From 8aaf4352ed2ebb4261372bdaf14a8f12751eaed6 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 10 Apr 2026 15:01:49 +0200 Subject: [PATCH] feat(26-03): rewrite landing page as public discovery page - Replace DashboardPage with LandingPage using discovery hooks - Add HeroSection with Discover Gear heading and catalog search trigger - Add PopularSetupsSection using useDiscoverySetups with PublicSetupCard - Add RecentItemsSection using useDiscoveryItems with GlobalItemCard - Add TrendingCategoriesSection using useDiscoveryCategories with pills - Conditional Go to Collection CTA for authenticated users - Loading skeletons with animate-pulse for all three sections - Empty state handling: sections return null when no data - SectionSkeleton helper for consistent loading states - All clickable elements have cursor-pointer --- src/client/routes/index.tsx | 232 ++++++++++++++++++++++++++++-------- 1 file changed, 181 insertions(+), 51 deletions(-) diff --git a/src/client/routes/index.tsx b/src/client/routes/index.tsx index 2dc935c..936b249 100644 --- a/src/client/routes/index.tsx +++ b/src/client/routes/index.tsx @@ -1,61 +1,191 @@ -import { createFileRoute } from "@tanstack/react-router"; -import { DashboardCard } from "../components/DashboardCard"; -import { useFormatters } from "../hooks/useFormatters"; -import { useSetups } from "../hooks/useSetups"; -import { useThreads } from "../hooks/useThreads"; -import { useTotals } from "../hooks/useTotals"; +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 { + useDiscoveryCategories, + useDiscoveryItems, + useDiscoverySetups, +} from "../hooks/useDiscovery"; +import { useUIStore } from "../stores/uiStore"; export const Route = createFileRoute("/")({ - component: DashboardPage, + component: LandingPage, }); -function DashboardPage() { - const { data: totals } = useTotals(); - const { data: threads } = useThreads(false); - const { data: setups } = useSetups(); - const { weight, price } = useFormatters(); - - const global = totals?.global; - const activeThreadCount = threads?.length ?? 0; - const setupCount = setups?.length ?? 0; +function LandingPage() { + const { data: auth } = useAuth(); + const isAuthenticated = !!auth?.user; + const openCatalogSearch = useUIStore((s) => s.openCatalogSearch); return (
-
- - - -
+ openCatalogSearch("collection")} + /> + + + +
+ ); +} + +function HeroSection({ + isAuthenticated, + onSearchFocus, +}: { + isAuthenticated: boolean; + onSearchFocus: () => void; +}) { + return ( +
+

+ Discover Gear +

+

Browse what other people carry

+
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 the catalog... + +
+ {isAuthenticated && ( +
+ + Go to Collection + +
+ )} +
+ ); +} + +function PopularSetupsSection() { + const { data, isLoading } = useDiscoverySetups(6); + const setups = data?.items ?? []; + + if (!isLoading && setups.length === 0) return null; + + return ( +
+
+

Popular Setups

+
+ {isLoading ? ( + + ) : ( +
+ {setups.map((setup) => ( + + ))} +
+ )} +
+ ); +} + +function RecentItemsSection() { + const { data, isLoading } = useDiscoveryItems(8); + const items = data?.items ?? []; + + if (!isLoading && items.length === 0) return null; + + return ( +
+
+

Recently Added

+
+ {isLoading ? ( + + ) : ( +
+ {items.map((item) => ( + + ))} +
+ )} +
+ ); +} + +function TrendingCategoriesSection() { + const { data, isLoading } = useDiscoveryCategories(12); + const categories = data ?? []; + + if (!isLoading && categories.length === 0) return null; + + return ( +
+
+

+ Trending Categories +

+
+ {isLoading ? ( +
+ {Array.from({ length: 8 }).map((_, i) => ( +
+ ))} +
+ ) : ( +
+ {categories.map((cat) => ( + + {cat.name} + {cat.itemCount} + + ))} +
+ )} +
+ ); +} + +function SectionSkeleton({ count, aspect }: { count: number; aspect: string }) { + return ( +
+ {Array.from({ length: count }).map((_, i) => ( +
+ {aspect !== "none" && ( +
+ )} +
+
+
+
+
+ ))}
); }