diff --git a/site/src/app/create/page.tsx b/site/src/app/create/page.tsx index 4ca0fbb..d5af55d 100644 --- a/site/src/app/create/page.tsx +++ b/site/src/app/create/page.tsx @@ -172,7 +172,7 @@ git tag v1.0.0 && git push origin v1.0.0`} export default function CreatePage() { return ( -
+

Create a Sound Pack

diff --git a/site/src/app/integrate/page.tsx b/site/src/app/integrate/page.tsx index d99af4b..5881952 100644 --- a/site/src/app/integrate/page.tsx +++ b/site/src/app/integrate/page.tsx @@ -28,7 +28,7 @@ const AGENT_SKILL = fs.readFileSync( export default function IntegratePage() { return ( -
+

Add Sound Packs to Your CLI

diff --git a/site/src/app/not-found.tsx b/site/src/app/not-found.tsx index a0ec041..0348211 100644 --- a/site/src/app/not-found.tsx +++ b/site/src/app/not-found.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; export default function NotFound() { return ( -
+

404

Pack not found. Maybe it wandered off to gather resources. diff --git a/site/src/app/packs/PacksClient.tsx b/site/src/app/packs/PacksClient.tsx index 092c6f9..bbb22ad 100644 --- a/site/src/app/packs/PacksClient.tsx +++ b/site/src/app/packs/PacksClient.tsx @@ -1,6 +1,13 @@ "use client"; -import { useState, useMemo, useCallback, useEffect, startTransition } from "react"; +import { + useState, + useMemo, + useCallback, + useEffect, + useRef, + startTransition, +} from "react"; import { useSearchParams, useRouter } from "next/navigation"; import type { PackMeta } from "@/lib/types"; import { LANGUAGE_LABELS } from "@/lib/constants"; @@ -12,7 +19,13 @@ import { ErrorBoundary } from "@/components/ui/ErrorBoundary"; const PACKS_PER_PAGE = 24; -type SortKey = "name-asc" | "name-desc" | "sounds-desc" | "sounds-asc" | "date-desc" | "date-asc"; +type SortKey = + | "name-asc" + | "name-desc" + | "sounds-desc" + | "sounds-asc" + | "date-desc" + | "date-asc"; const SORT_OPTIONS: { value: SortKey; label: string }[] = [ { value: "name-asc", label: "A → Z" }, @@ -64,18 +77,24 @@ export function PacksClient({ packs: allPacks }: { packs: PackMeta[] }) { // Read initial state from URL const [query, setQuery] = useState(searchParams.get("q") || ""); const [activeTag, setActiveTag] = useState( - searchParams.get("tag") || null + searchParams.get("tag") || null, ); const [activeLang, setActiveLang] = useState( - searchParams.get("lang") || null + searchParams.get("lang") || null, + ); + const [activeFranchise, setActiveFranchise] = useState( + searchParams.get("franchise") || null, ); const [sortKey, setSortKey] = useState( - (searchParams.get("sort") as SortKey) || "date-desc" + (searchParams.get("sort") as SortKey) || "date-desc", ); const [page, setPage] = useState( - Math.max(1, parseInt(searchParams.get("page") || "1", 10) || 1) + Math.max(1, parseInt(searchParams.get("page") || "1", 10) || 1), ); - const [tagsExpanded, setTagsExpanded] = useState(false); + const [expandedFilter, setExpandedFilter] = useState< + "franchises" | "tags" | "langs" | null + >(null); + const gridRef = useRef(null); // Sync state → URL const updateUrl = useCallback( @@ -94,7 +113,7 @@ export function PacksClient({ packs: allPacks }: { packs: PackMeta[] }) { const qs = params.toString(); router.replace(qs ? `?${qs}` : "/packs", { scroll: false }); }, - [searchParams, router] + [searchParams, router], ); // Wrapped setters that also update URL @@ -104,7 +123,7 @@ export function PacksClient({ packs: allPacks }: { packs: PackMeta[] }) { setPage(1); updateUrl({ q: q || null, page: null }); }, - [updateUrl] + [updateUrl], ); const handleSetTag = useCallback( @@ -113,7 +132,7 @@ export function PacksClient({ packs: allPacks }: { packs: PackMeta[] }) { setPage(1); updateUrl({ tag, page: null }); }, - [updateUrl] + [updateUrl], ); const handleSetLang = useCallback( @@ -122,7 +141,16 @@ export function PacksClient({ packs: allPacks }: { packs: PackMeta[] }) { setPage(1); updateUrl({ lang, page: null }); }, - [updateUrl] + [updateUrl], + ); + + const handleSetFranchise = useCallback( + (franchise: string | null) => { + setActiveFranchise(franchise); + setPage(1); + updateUrl({ franchise, page: null }); + }, + [updateUrl], ); const handleSetSort = useCallback( @@ -131,44 +159,69 @@ export function PacksClient({ packs: allPacks }: { packs: PackMeta[] }) { setPage(1); updateUrl({ sort: sort === "date-desc" ? null : sort, page: null }); }, - [updateUrl] + [updateUrl], ); const handleSetPage = useCallback( (p: number) => { setPage(p); updateUrl({ page: p === 1 ? null : String(p) }); - window.scrollTo({ top: 0, behavior: "smooth" }); + gridRef.current?.scrollIntoView({ behavior: "smooth" }); }, - [updateUrl] + [updateUrl], ); - // Derive tag and language counts - const { allTags, allLangs } = useMemo(() => { + // Derive tag, language, and franchise counts + const { allTags, allLangs, allFranchises } = useMemo(() => { const tagCounts = new Map(); const langCounts = new Map(); + const franchiseCounts = new Map(); for (const p of allPacks) { for (const t of p.tags || []) { tagCounts.set(t, (tagCounts.get(t) || 0) + 1); } langCounts.set(p.language, (langCounts.get(p.language) || 0) + 1); + if (p.franchise.name && p.franchise.name !== "Unknown") { + franchiseCounts.set( + p.franchise.name, + (franchiseCounts.get(p.franchise.name) || 0) + 1, + ); + } } return { allTags: [...tagCounts.entries()].sort((a, b) => b[1] - a[1]), allLangs: [...langCounts.entries()].sort((a, b) => b[1] - a[1]), + allFranchises: [...franchiseCounts.entries()].sort((a, b) => b[1] - a[1]), }; }, [allPacks]); - // Visible tags (collapsed = only tags with count >= 3, plus active tag) - const TAG_MIN_COUNT = 3; - const visibleTags = useMemo(() => { - if (tagsExpanded) return allTags; - const filtered = allTags.filter( - ([tag, count]) => count >= TAG_MIN_COUNT || tag === activeTag - ); - return filtered; - }, [allTags, tagsExpanded, activeTag]); - const hiddenTagCount = allTags.length - visibleTags.length; + const tagsRef = useRef(null); + const langsRef = useRef(null); + const franchisesRef = useRef(null); + const [tagsOverflow, setTagsOverflow] = useState(false); + const [langsOverflow, setLangsOverflow] = useState(false); + const [franchisesOverflow, setFranchisesOverflow] = useState(false); + + useEffect(() => { + const check = () => { + if (tagsRef.current) + setTagsOverflow( + tagsRef.current.scrollHeight > tagsRef.current.clientHeight, + ); + if (langsRef.current) + setLangsOverflow( + langsRef.current.scrollHeight > langsRef.current.clientHeight, + ); + if (franchisesRef.current) + setFranchisesOverflow( + franchisesRef.current.scrollHeight > + franchisesRef.current.clientHeight, + ); + }; + check(); + window.addEventListener("resize", check); + return () => window.removeEventListener("resize", check); + }, [allTags, allLangs, allFranchises]); // Filter + sort const filtered = useMemo(() => { @@ -180,6 +233,9 @@ export function PacksClient({ packs: allPacks }: { packs: PackMeta[] }) { if (activeLang) { packs = packs.filter((p) => p.language === activeLang); } + if (activeFranchise) { + packs = packs.filter((p) => p.franchise.name === activeFranchise); + } if (query) { const q = query.toLowerCase(); packs = packs.filter((pack) => { @@ -200,14 +256,14 @@ export function PacksClient({ packs: allPacks }: { packs: PackMeta[] }) { } return sortPacks(packs, sortKey); - }, [allPacks, query, activeTag, activeLang, sortKey]); + }, [allPacks, query, activeTag, activeLang, activeFranchise, sortKey]); // Pagination const totalPages = Math.max(1, Math.ceil(filtered.length / PACKS_PER_PAGE)); const safePage = Math.min(page, totalPages); const paginatedPacks = filtered.slice( (safePage - 1) * PACKS_PER_PAGE, - safePage * PACKS_PER_PAGE + safePage * PACKS_PER_PAGE, ); // Reset page if it exceeds total after filter change @@ -218,7 +274,7 @@ export function PacksClient({ packs: allPacks }: { packs: PackMeta[] }) { }, [page, totalPages]); return ( -

+

Sound Packs

@@ -226,76 +282,176 @@ export function PacksClient({ packs: allPacks }: { packs: PackMeta[] }) { {allPacks.length} CESP-compatible sound packs for your IDE.

- {/* Tag pills */} - {allTags.length > 0 && ( -
- - Tags - -
- handleSetTag(null)} - /> - {visibleTags.map(([tag, count]) => ( - - handleSetTag(activeTag === tag ? null : tag) - } - /> - ))} - {allTags.length > visibleTags.length || tagsExpanded ? ( - - ) : null} + handleSetFranchise(null)} + variant="franchise" + /> + {allFranchises.map(([franchise, count]) => ( + + handleSetFranchise( + activeFranchise === franchise ? null : franchise, + ) + } + variant="franchise" + /> + ))} +
+ {(franchisesOverflow || expandedFilter === "franchises") && ( + + )} +
-
- )} + )} - {/* Language pills */} - {allLangs.length > 0 && ( -
- - Lang - -
- handleSetLang(null)} - variant="lang" - /> - {allLangs.map(([lang, count]) => ( - - handleSetLang(activeLang === lang ? null : lang) - } - variant="lang" - /> - ))} + {/* Tag pills */} + {allTags.length > 0 && ( +
+ + Tags ({allTags.length}) + +
+
+ handleSetTag(null)} + /> + {allTags.map(([tag, count]) => ( + handleSetTag(activeTag === tag ? null : tag)} + /> + ))} +
+ {(tagsOverflow || expandedFilter === "tags") && ( + + )} +
-
- )} + )} + + {/* Language pills */} + {allLangs.length > 0 && ( +
+ + Languages{" "} + ({allLangs.length}) + +
+
+ handleSetLang(null)} + variant="lang" + /> + {allLangs.map(([lang, count]) => ( + + handleSetLang(activeLang === lang ? null : lang) + } + variant="lang" + /> + ))} +
+ {(langsOverflow || expandedFilter === "langs") && ( + + )} +
+
+ )} +
{/* Search + Sort */} -
+
-
+

Showing {(safePage - 1) * PACKS_PER_PAGE + 1}– {Math.min(safePage * PACKS_PER_PAGE, filtered.length)} of{" "} @@ -356,34 +515,69 @@ export function PacksClient({ packs: allPacks }: { packs: PackMeta[] }) { {/* Pagination */} {totalPages > 1 && ( -

- - {Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => ( +
+ {/* Desktop: single row */} +
- ))} - + {Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => ( + + ))} + +
+ {/* Mobile: stacked */} +
+ +
+ {Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => ( + + ))} +
+ +
)}
@@ -394,8 +588,10 @@ export function PacksClient({ packs: allPacks }: { packs: PackMeta[] }) { const PILL_INACTIVE_STYLES = { default: - "border-surface-border text-text-subtle hover:border-gold/50 hover:text-text-muted", + "border-violet-700/50 text-violet-400/70 hover:border-violet-500/50 hover:text-violet-300", lang: "border-amber-700/50 text-amber-500/70 hover:border-gold/50 hover:text-gold", + franchise: + "border-emerald-700/50 text-emerald-400/70 hover:border-emerald-500/50 hover:text-emerald-300", }; function FilterPill({ @@ -409,19 +605,22 @@ function FilterPill({ count: number; active: boolean; onClick: () => void; - variant?: "default" | "lang"; + variant?: "default" | "lang" | "franchise"; }) { return ( ); } diff --git a/site/src/app/packs/[name]/page.tsx b/site/src/app/packs/[name]/page.tsx index 76f0d1e..5d1d000 100644 --- a/site/src/app/packs/[name]/page.tsx +++ b/site/src/app/packs/[name]/page.tsx @@ -47,7 +47,7 @@ export default async function PackDetailPage({ const next = idx < allPacks.length - 1 ? allPacks[idx + 1] : null; return ( -
+