chrysopedia/frontend/src/pages/CreatorDetail.tsx
jlightner 78eca0d546 feat: Replaced compact creator header with full hero section: 96px avat…
- "frontend/src/pages/CreatorDetail.tsx"
- "frontend/src/App.css"

GSD-Task: S01/T02
2026-04-03 08:52:06 +00:00

191 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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: "AZ" },
];
export default function CreatorDetail() {
const { slug } = useParams<{ slug: string }>();
const [creator, setCreator] = useState<CreatorDetailResponse | null>(null);
const [loading, setLoading] = useState(true);
const [notFound, setNotFound] = useState(false);
const [error, setError] = useState<string | null>(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 <div className="loading">Loading creator</div>;
}
if (notFound) {
return (
<div className="technique-404">
<h2>Creator Not Found</h2>
<p>The creator "{slug}" doesn't exist.</p>
<Link to="/creators" className="btn">
Back to Creators
</Link>
</div>
);
}
if (error || !creator) {
return (
<div className="loading error-text">
Error: {error ?? "Unknown error"}
</div>
);
}
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 (
<div className="creator-detail">
<Link to="/creators" className="back-link">
← Creators
</Link>
{/* Hero */}
<header className="creator-hero">
<CreatorAvatar
creatorId={creator.id}
name={creator.name}
imageUrl={creator.avatar_url ?? undefined}
size={96}
/>
<div className="creator-hero__info">
<h1 className="creator-hero__name">{creator.name}</h1>
{creator.bio && (
<p className="creator-hero__bio">{creator.bio}</p>
)}
{creator.genres && creator.genres.length > 0 && (
<div className="creator-hero__genres">
{creator.genres.map((g) => (
<span key={g} className="pill">{g}</span>
))}
</div>
)}
</div>
</header>
{/* Stats */}
<div className="creator-detail__stats-bar">
<span className="creator-detail__stats">
{creator.video_count} video{creator.video_count !== 1 ? "s" : ""}
</span>
{Object.keys(creator.genre_breakdown).length > 0 && (
<span className="creator-detail__topic-pills">
{Object.entries(creator.genre_breakdown)
.sort(([, a], [, b]) => b - a)
.map(([cat, count]) => (
<span key={cat} className={`badge badge--cat-${catSlug(cat)}`}>
{cat}: {count}
</span>
))}
</span>
)}
</div>
{/* Technique pages */}
<section className="creator-techniques">
<div className="creator-techniques__header">
<h2 className="creator-techniques__title">
Techniques ({creator.technique_count})
</h2>
<SortDropdown
options={CREATOR_SORT_OPTIONS}
value={sort}
onChange={setSort}
/>
</div>
{techniques.length === 0 ? (
<div className="empty-state">No techniques yet.</div>
) : (
<div className="creator-techniques__list">
{techniques.map((t, i) => (
<Link
key={t.slug}
to={`/techniques/${t.slug}`}
className="creator-technique-card card-stagger"
style={{ '--stagger-index': i } as React.CSSProperties}
>
<span className="creator-technique-card__title">
{t.title}
</span>
<span className="creator-technique-card__meta">
<span className="badge badge--category">
{t.topic_category}
</span>
</span>
</Link>
))}
</div>
)}
</section>
</div>
);
}