"use client"; import { useMemo, useCallback, useEffect, useRef, useState } from "react"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import posthog from "posthog-js"; import { ArrowDownAZ, ArrowDownZA, ArrowUpDown, Clock, Grid3X3, LayoutGrid, X } from "lucide-react"; import type { Collection, IconEntry } from "@/lib/icons"; import { loadIconsManifest, prefetchIconsManifest } from "@/lib/icons-manifest"; import { Sidebar } from "@/components/layout/sidebar"; import { IconGrid } from "@/components/icons/icon-grid"; import { HomeHero } from "@/components/home-hero"; import { OnboardingHint } from "@/components/onboarding-hint"; import { HelpFab } from "@/components/help-fab"; import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"; import { useFavoritesStore } from "@/lib/stores/favorites-store"; import { useSidebarStore } from "@/lib/stores/sidebar-store"; import { useSearchStore } from "@/lib/stores/search-store"; const SORT_OPTIONS = ["default", "recent", "az", "za"] as const; interface HomeContentProps { categoryCounts: { name: string; count: number }[]; count: number; recentIcons: IconEntry[]; collections: { name: Collection; count: number }[]; defaultCollection?: Collection; defaultCategory?: string; defaultCategorySlug?: string; } export function HomeContent({ categoryCounts, count, recentIcons, collections, defaultCollection, defaultCategory, defaultCategorySlug }: HomeContentProps) { const router = useRouter(); const searchParams = useSearchParams(); const sidebarOpen = useSidebarStore((s) => s.open); const setSidebarOpen = useSidebarStore((s) => s.setOpen); const favorites = useFavoritesStore((s) => s.favorites); const globalQuery = useSearchStore((s) => s.query); const setGlobalQuery = useSearchStore((s) => s.setQuery); // Read URL params const queryParam = searchParams.get("q") || ""; const categoryParam = searchParams.get("category") || defaultCategory || null; const sortParam = searchParams.get("sort"); const viewParam = (searchParams.get("view") || "comfortable") as "compact" | "comfortable"; const favoritesParam = searchParams.get("favorites") === "true"; const collectionParam = (searchParams.get("collection") || defaultCollection || null) as Collection | null; const query = globalQuery; const debounceRef = useRef>(null); const isDefaultView = !query.trim() && !categoryParam && !sortParam && !favoritesParam && (!collectionParam || !!defaultCollection); // Lazy-loaded full icons manifest. null = not yet loaded; [] = loading/empty. const [allIcons, setAllIcons] = useState(null); const [manifestError, setManifestError] = useState(false); // Prefetch the manifest while the hero is shown so it's ready when the user searches. useEffect(() => { if (isDefaultView) { prefetchIconsManifest(); } }, [isDefaultView]); // Load the manifest when the user leaves the default (hero) view. useEffect(() => { if (!isDefaultView && allIcons === null && !manifestError) { loadIconsManifest() .then((icons) => setAllIcons(icons)) .catch(() => setManifestError(true)); } }, [isDefaultView, allIcons, manifestError]); // Sync global store from URL (e.g. shared link) useEffect(() => { setGlobalQuery(queryParam); }, [queryParam, setGlobalQuery]); const updateUrl = useCallback( (updates: Record) => { const params = new URLSearchParams(searchParams.toString()); for (const [key, value] of Object.entries(updates)) { if (value === null || value === "") { params.delete(key); } else { params.set(key, value); } } // On /collection/[name] and /category/[slug] pages, keep the base path // and append query params so updates don't navigate away from the landing. const basePath = defaultCategorySlug ? `/category/${defaultCategorySlug}` : defaultCollection ? `/collection/${defaultCollection}` : "/"; const qs = params.toString(); router.replace(qs ? `${basePath}?${qs}` : basePath, { scroll: false }); }, [router, searchParams, defaultCollection, defaultCategorySlug] ); // Sync global search store changes to URL with debounce useEffect(() => { // Skip if query already matches URL to avoid loops const currentUrlQuery = searchParams.get("q") || ""; if (globalQuery === currentUrlQuery) return; if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => { updateUrl({ q: globalQuery || null }); }, 400); return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; }, [globalQuery, updateUrl, searchParams]); const handleCategorySelect = useCallback( (category: string | null) => { updateUrl({ category, favorites: null }); setSidebarOpen(false); }, [updateUrl, setSidebarOpen] ); const handleCollectionSelect = useCallback( (collection: Collection | null) => { setSidebarOpen(false); posthog.capture("collection_switched", { collection: collection ?? "all" }); if (collection) { router.push(`/collection/${collection}`); } else { router.push("/"); } }, [router, setSidebarOpen] ); const handleToggleFavorites = useCallback(() => { updateUrl({ favorites: favoritesParam ? null : "true", category: null, }); setSidebarOpen(false); }, [updateUrl, favoritesParam, setSidebarOpen]); const handleSortCycle = useCallback(() => { const current = sortParam || "default"; const idx = SORT_OPTIONS.indexOf(current as typeof SORT_OPTIONS[number]); const next = SORT_OPTIONS[(idx + 1) % SORT_OPTIONS.length]; updateUrl({ sort: next === "default" ? null : next }); }, [updateUrl, sortParam]); const handleViewToggle = useCallback(() => { updateUrl({ view: viewParam === "compact" ? null : "compact" }); }, [updateUrl, viewParam]); // Filter icons by collection first, then apply other filters. // allIcons is null until manifest is fetched. const collectionIcons = useMemo(() => { if (!allIcons) return null; if (!collectionParam) return allIcons; return allIcons.filter((icon) => icon.collection === collectionParam); }, [allIcons, collectionParam]); // Compute categories for the active collection const activeCategoryCounts = useMemo(() => { if (!collectionIcons || !collectionParam) return categoryCounts; const counts = new Map(); for (const icon of collectionIcons) { for (const c of icon.categories) { counts.set(c, (counts.get(c) || 0) + 1); } } return [...counts.entries()] .map(([name, count]) => ({ name, count })) .sort((a, b) => a.name.localeCompare(b.name)); }, [collectionParam, collectionIcons, categoryCounts]); const [filtered, setFiltered] = useState([]); const filterKey = `${query}|${categoryParam ?? ""}|${sortParam ?? ""}|${favoritesParam}|${collectionParam ?? ""}`; useEffect(() => { if (!collectionIcons) return; let active = true; let result = collectionIcons; // Favorites filter if (favoritesParam) { result = result.filter((icon) => favorites.includes(icon.slug)); } // Category filter if (categoryParam) { result = result.filter((icon) => icon.categories.some( (c) => c.toLowerCase() === categoryParam.toLowerCase() ) ); } // Lazy-load Fuse.js only when there is a search query. // Clear stale results immediately so the previous query's matches don't // linger while Fuse downloads / runs. if (query.trim()) { setFiltered([]); import("@/lib/search").then(({ searchIcons }) => { if (!active) return; let searched = searchIcons(result, query); if (sortParam === "az") { searched = [...searched].sort((a, b) => a.title.localeCompare(b.title)); } else if (sortParam === "za") { searched = [...searched].sort((a, b) => b.title.localeCompare(a.title)); } else if (sortParam === "recent") { searched = [...searched].sort((a, b) => (b.dateAdded ?? "").localeCompare(a.dateAdded ?? ""), ); } setFiltered(searched); }); return () => { active = false; }; } // Sort if (sortParam === "az") { result = [...result].sort((a, b) => a.title.localeCompare(b.title)); } else if (sortParam === "za") { result = [...result].sort((a, b) => b.title.localeCompare(a.title)); } else if (sortParam === "recent") { result = [...result].sort((a, b) => (b.dateAdded ?? "").localeCompare(a.dateAdded ?? ""), ); } setFiltered(result); return () => { active = false; }; // filterKey captures all search/filter/sort deps; collectionIcons and favorites are the data deps // eslint-disable-next-line react-hooks/exhaustive-deps }, [collectionIcons, filterKey, favorites]); const activeCount = collectionParam && collectionIcons ? collectionIcons.length : count; // Stable key for IconGrid so it remounts naturally when filters change, // resetting internal visibleCount without a secondary useEffect setState loop. const gridKey = filterKey; const sidebarContent = ( ); return ( <> {/* Desktop sidebar */} {sidebarContent} {/* Mobile sidebar Sheet */} Navigation {/* Main content area */}
{isDefaultView ? ( /* Hero landing */
{}} onCategorySelect={(cat) => updateUrl({ category: cat })} onCollectionSelect={(col) => router.push(`/collection/${col}`)} />
) : ( /* Filtered view */ <> {/* Sticky toolbar - count + controls */}
{/* Count label */}

{manifestError ? ( Failed to load icons ) : !allIcons ? ( Loading... ) : favoritesParam ? `${filtered.length} fav${filtered.length !== 1 ? "s" : ""}` : filtered.length === activeCount ? `${activeCount.toLocaleString()}` : `${filtered.length.toLocaleString()}/${activeCount.toLocaleString()}`} {categoryParam && ( {categoryParam} )}

{/* View + Sort controls */}
{/* Active filter chips */} {(collectionParam || categoryParam || favoritesParam || query.trim()) && (
{collectionParam && ( )} {categoryParam && ( )} {favoritesParam && ( )} {query.trim() && ( )}
)} {/* Featured callout — only on /category/google */} {defaultCategorySlug === "google" && (
New

Google’s 2026 refresh is here

13 redesigned product icons with the new gradient identity. First library to host them.

View 2026 set
)} {/* Grid */}
{manifestError ? (

Failed to load icons. Please refresh and try again.

) : !allIcons ? (
) : ( )}
)}
{/* First-visit onboarding hint */} {/* Persistent help button */} ); }