/** * Creator detail page. * * Shows creator info (name, genres, video/technique counts) and lists * their technique pages with links. Handles loading and 404 states. */ import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { fetchCreator, type CreatorDetailResponse, } from "../api/public-client"; import CreatorAvatar from "../components/CreatorAvatar"; import SortDropdown from "../components/SortDropdown"; import { catSlug } from "../utils/catSlug"; import { useDocumentTitle } from "../hooks/useDocumentTitle"; import { useSortPreference } from "../hooks/useSortPreference"; const CREATOR_SORT_OPTIONS = [ { value: "newest", label: "Newest" }, { value: "oldest", label: "Oldest" }, { value: "alpha", label: "A–Z" }, ]; export default function CreatorDetail() { const { slug } = useParams<{ slug: string }>(); const [creator, setCreator] = useState(null); const [loading, setLoading] = useState(true); const [notFound, setNotFound] = useState(false); const [error, setError] = useState(null); const [sort, setSort] = useSortPreference("newest"); useDocumentTitle(creator ? `${creator.name} — Chrysopedia` : "Chrysopedia"); useEffect(() => { if (!slug) return; let cancelled = false; setLoading(true); setNotFound(false); setError(null); void (async () => { try { const creatorData = await fetchCreator(slug); if (!cancelled) { setCreator(creatorData); } } catch (err) { if (!cancelled) { if (err instanceof Error && err.message.includes("404")) { setNotFound(true); } else { setError( err instanceof Error ? err.message : "Failed to load creator", ); } } } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [slug]); if (loading) { return
Loading creator…
; } if (notFound) { return (

Creator Not Found

The creator "{slug}" doesn't exist.

Back to Creators
); } if (error || !creator) { return (
Error: {error ?? "Unknown error"}
); } const techniques = [...creator.techniques].sort((a, b) => { switch (sort) { case "oldest": return a.created_at.localeCompare(b.created_at); case "alpha": return a.title.localeCompare(b.title); case "newest": default: return b.created_at.localeCompare(a.created_at); } }); return (
← Creators {/* Hero */}

{creator.name}

{creator.bio && (

{creator.bio}

)} {creator.genres && creator.genres.length > 0 && (
{creator.genres.map((g) => ( {g} ))}
)}
{/* Stats */}
{creator.video_count} video{creator.video_count !== 1 ? "s" : ""} {Object.keys(creator.genre_breakdown).length > 0 && ( {Object.entries(creator.genre_breakdown) .sort(([, a], [, b]) => b - a) .map(([cat, count]) => ( {cat}: {count} ))} )}
{/* Technique pages */}

Techniques ({creator.technique_count})

{techniques.length === 0 ? (
No techniques yet.
) : (
{techniques.map((t, i) => ( {t.title} {t.topic_category} ))}
)}
); }