/** * Sub-topic detail page. * * Shows techniques for a specific sub-topic within a category, * grouped by creator. Breadcrumb navigation back to Topics. */ import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { fetchSubTopicTechniques, type TechniqueListItem, } from "../api"; import { catSlug } from "../utils/catSlug"; import SortDropdown from "../components/SortDropdown"; import TagList from "../components/TagList"; import { useDocumentTitle } from "../hooks/useDocumentTitle"; import { useSortPreference } from "../hooks/useSortPreference"; const SUBTOPIC_SORT_OPTIONS = [ { value: "alpha", label: "A–Z" }, { value: "newest", label: "Newest" }, { value: "oldest", label: "Oldest" }, { value: "creator", label: "Creator" }, ]; /** Convert a URL slug to a display name: replace hyphens with spaces, title-case. */ function slugToDisplayName(slug: string): string { return slug .replace(/-/g, " ") .replace(/\b\w/g, (c) => c.toUpperCase()); } /** Group techniques by creator_name, preserving order of first appearance. */ function groupByCreator( items: TechniqueListItem[], ): { creatorName: string; creatorSlug: string; techniques: TechniqueListItem[] }[] { const map = new Map(); for (const item of items) { const name = item.creator_name || "Unknown"; const existing = map.get(name); if (existing) { existing.techniques.push(item); } else { map.set(name, { creatorName: name, creatorSlug: item.creator_slug || "", techniques: [item], }); } } return Array.from(map.values()); } export default function SubTopicPage() { const { category, subtopic } = useParams<{ category: string; subtopic: string }>(); const [techniques, setTechniques] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [sort, setSort] = useSortPreference("alpha"); const categoryDisplay = category ? slugToDisplayName(category) : ""; const subtopicDisplay = subtopic ? slugToDisplayName(subtopic) : ""; useDocumentTitle( subtopicDisplay && categoryDisplay ? `${subtopicDisplay} — ${categoryDisplay} — Chrysopedia` : "Chrysopedia", ); useEffect(() => { if (!category || !subtopic) return; let cancelled = false; setLoading(true); setError(null); void (async () => { try { const data = await fetchSubTopicTechniques(category, subtopic, { limit: 100, sort }); if (!cancelled) { setTechniques(data.items); } } catch (err) { if (!cancelled) { setError( err instanceof Error ? err.message : "Failed to load techniques", ); } } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [category, subtopic, sort]); if (loading) { return
Loading techniques…
; } if (error) { return (
Error: {error}
); } const groups = groupByCreator(techniques); const slug = category ? catSlug(categoryDisplay) : ""; return (
{/* Breadcrumbs */}

{subtopicDisplay}

{categoryDisplay} · {techniques.length} technique{techniques.length !== 1 ? "s" : ""}

{techniques.length === 0 ? (
No techniques found for this sub-topic.
) : (
{groups.map((group) => (

{group.creatorSlug ? ( {group.creatorName} ) : ( group.creatorName )} {group.techniques.length} technique{group.techniques.length !== 1 ? "s" : ""}

{group.techniques.map((t, i) => ( {t.title} {t.topic_tags && t.topic_tags.length > 0 && ( )} {t.summary && ( {t.summary.length > 150 ? `${t.summary.slice(0, 150)}…` : t.summary} )} ))}
))}
)}
); }