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 ? (
-