/** * 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, updateCreatorProfile, followCreator, unfollowCreator, getFollowStatus, type CreatorDetailResponse, } from "../api"; import { useAuth } from "../context/AuthContext"; import CreatorAvatar from "../components/CreatorAvatar"; import { SocialIcon } from "../components/SocialIcons"; import ChatWidget from "../components/ChatWidget"; import PostsFeed from "../components/PostsFeed"; import PersonalityProfile from "../components/PersonalityProfile"; import SortDropdown from "../components/SortDropdown"; import TagList from "../components/TagList"; 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" }, ]; const PLATFORM_OPTIONS = [ "Instagram", "YouTube", "Bandcamp", "SoundCloud", "Twitter", "Spotify", "Facebook", "Twitch", "Website", ]; export default function CreatorDetail() { const { slug } = useParams<{ slug: string }>(); const { isAuthenticated } = useAuth(); 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"); // Follow state const [following, setFollowing] = useState(false); const [followLoading, setFollowLoading] = useState(false); const [followerCount, setFollowerCount] = useState(0); // Edit mode state const [editMode, setEditMode] = useState(false); const [editBio, setEditBio] = useState(""); const [editLinks, setEditLinks] = useState>([]); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); 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]); // Check follow status when creator loads and user is authenticated useEffect(() => { if (!creator || !isAuthenticated) { setFollowing(false); return; } setFollowerCount(creator.follower_count); let cancelled = false; void (async () => { try { const status = await getFollowStatus(creator.id); if (!cancelled) setFollowing(status.following); } catch { // Non-critical — leave as not following } })(); return () => { cancelled = true; }; }, [creator?.id, isAuthenticated]); // eslint-disable-line react-hooks/exhaustive-deps async function handleFollowToggle() { if (!creator || followLoading) return; setFollowLoading(true); try { const res = following ? await unfollowCreator(creator.id) : await followCreator(creator.id); setFollowing(res.followed); setFollowerCount(res.follower_count); } catch { // Silently fail — button state stays as-is } finally { setFollowLoading(false); } } function enterEditMode() { if (!creator) return; setEditBio(creator.bio ?? ""); setEditLinks( creator.social_links && Object.keys(creator.social_links).length > 0 ? Object.entries(creator.social_links).map(([platform, url]) => ({ platform, url })) : [], ); setSaveError(null); setEditMode(true); } function handleCancel() { setEditMode(false); setSaveError(null); } async function handleSave() { if (!creator) return; setSaving(true); setSaveError(null); const filteredLinks = editLinks.filter((l) => l.platform && l.url); const socialLinks: Record | null = filteredLinks.length > 0 ? Object.fromEntries(filteredLinks.map((l) => [l.platform.toLowerCase(), l.url])) : null; try { await updateCreatorProfile(creator.id, { bio: editBio || null, social_links: socialLinks, }); // Optimistic local update setCreator({ ...creator, bio: editBio || null, social_links: socialLinks, }); setEditMode(false); } catch (err) { setSaveError(err instanceof Error ? err.message : "Failed to save"); } finally { setSaving(false); } } function addLinkRow() { setEditLinks([...editLinks, { platform: PLATFORM_OPTIONS[0] ?? "Instagram", url: "" }]); } function removeLinkRow(index: number) { setEditLinks(editLinks.filter((_, i) => i !== index)); } function updateLinkRow(index: number, field: "platform" | "url", value: string) { setEditLinks(editLinks.map((row, i) => (i === index ? { ...row, [field]: value } : row))); } 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}

{!editMode && ( <> {isAuthenticated && ( )} )}
{editMode ? (