feat: Synced CreatorDetailResponse with backend schema (7 new fields) a…

- "frontend/src/api/public-client.ts"
- "frontend/src/pages/CreatorDetail.tsx"

GSD-Task: S01/T01
This commit is contained in:
jlightner 2026-04-03 08:50:12 +00:00
parent 3807db8028
commit df0ff28f5e
2 changed files with 33 additions and 32 deletions

View file

@ -174,6 +174,13 @@ export interface CreatorBrowseResponse {
limit: number; limit: number;
} }
export interface CreatorTechniqueItem {
title: string;
slug: string;
topic_category: string;
created_at: string;
}
export interface CreatorDetailResponse { export interface CreatorDetailResponse {
id: string; id: string;
name: string; name: string;
@ -184,6 +191,13 @@ export interface CreatorDetailResponse {
created_at: string; created_at: string;
updated_at: string; updated_at: string;
video_count: number; video_count: number;
bio: string | null;
social_links: Record<string, string> | null;
featured: boolean;
avatar_url: string | null;
technique_count: number;
techniques: CreatorTechniqueItem[];
genre_breakdown: Record<string, number>;
} }
// ── Helpers ────────────────────────────────────────────────────────────────── // ── Helpers ──────────────────────────────────────────────────────────────────

View file

@ -9,14 +9,12 @@ import { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
import { import {
fetchCreator, fetchCreator,
fetchTechniques,
type CreatorDetailResponse, type CreatorDetailResponse,
type TechniqueListItem, type CreatorTechniqueItem,
} from "../api/public-client"; } from "../api/public-client";
import CreatorAvatar from "../components/CreatorAvatar"; import CreatorAvatar from "../components/CreatorAvatar";
import SortDropdown from "../components/SortDropdown"; import SortDropdown from "../components/SortDropdown";
import { catSlug } from "../utils/catSlug"; import { catSlug } from "../utils/catSlug";
import TagList from "../components/TagList";
import { useDocumentTitle } from "../hooks/useDocumentTitle"; import { useDocumentTitle } from "../hooks/useDocumentTitle";
import { useSortPreference } from "../hooks/useSortPreference"; import { useSortPreference } from "../hooks/useSortPreference";
@ -29,7 +27,6 @@ const CREATOR_SORT_OPTIONS = [
export default function CreatorDetail() { export default function CreatorDetail() {
const { slug } = useParams<{ slug: string }>(); const { slug } = useParams<{ slug: string }>();
const [creator, setCreator] = useState<CreatorDetailResponse | null>(null); const [creator, setCreator] = useState<CreatorDetailResponse | null>(null);
const [techniques, setTechniques] = useState<TechniqueListItem[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [notFound, setNotFound] = useState(false); const [notFound, setNotFound] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -47,13 +44,9 @@ export default function CreatorDetail() {
void (async () => { void (async () => {
try { try {
const [creatorData, techData] = await Promise.all([ const creatorData = await fetchCreator(slug);
fetchCreator(slug),
fetchTechniques({ creator_slug: slug, limit: 100, sort }),
]);
if (!cancelled) { if (!cancelled) {
setCreator(creatorData); setCreator(creatorData);
setTechniques(techData.items);
} }
} catch (err) { } catch (err) {
if (!cancelled) { if (!cancelled) {
@ -73,7 +66,7 @@ export default function CreatorDetail() {
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [slug, sort]); }, [slug]);
if (loading) { if (loading) {
return <div className="loading">Loading creator</div>; return <div className="loading">Loading creator</div>;
@ -99,6 +92,18 @@ export default function CreatorDetail() {
); );
} }
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 ( return (
<div className="creator-detail"> <div className="creator-detail">
<Link to="/creators" className="back-link"> <Link to="/creators" className="back-link">
@ -120,17 +125,11 @@ export default function CreatorDetail() {
)} )}
<span className="creator-detail__stats"> <span className="creator-detail__stats">
{creator.video_count} video{creator.video_count !== 1 ? "s" : ""} {creator.video_count} video{creator.video_count !== 1 ? "s" : ""}
{techniques.length > 0 && ( {Object.keys(creator.genre_breakdown).length > 0 && (
<> <>
<span className="queue-card__separator">·</span> <span className="queue-card__separator">·</span>
<span className="creator-detail__topic-pills"> <span className="creator-detail__topic-pills">
{Object.entries( {Object.entries(creator.genre_breakdown)
techniques.reduce<Record<string, number>>((acc, t) => {
const cat = t.topic_category || "Uncategorized";
acc[cat] = (acc[cat] || 0) + 1;
return acc;
}, {}),
)
.sort(([, a], [, b]) => b - a) .sort(([, a], [, b]) => b - a)
.map(([cat, count]) => ( .map(([cat, count]) => (
<span key={cat} className={`badge badge--cat-${catSlug(cat)}`}> <span key={cat} className={`badge badge--cat-${catSlug(cat)}`}>
@ -148,7 +147,7 @@ export default function CreatorDetail() {
<section className="creator-techniques"> <section className="creator-techniques">
<div className="creator-techniques__header"> <div className="creator-techniques__header">
<h2 className="creator-techniques__title"> <h2 className="creator-techniques__title">
Techniques ({techniques.length}) Techniques ({creator.technique_count})
</h2> </h2>
<SortDropdown <SortDropdown
options={CREATOR_SORT_OPTIONS} options={CREATOR_SORT_OPTIONS}
@ -162,7 +161,7 @@ export default function CreatorDetail() {
<div className="creator-techniques__list"> <div className="creator-techniques__list">
{techniques.map((t, i) => ( {techniques.map((t, i) => (
<Link <Link
key={t.id} key={t.slug}
to={`/techniques/${t.slug}`} to={`/techniques/${t.slug}`}
className="creator-technique-card card-stagger" className="creator-technique-card card-stagger"
style={{ '--stagger-index': i } as React.CSSProperties} style={{ '--stagger-index': i } as React.CSSProperties}
@ -174,19 +173,7 @@ export default function CreatorDetail() {
<span className="badge badge--category"> <span className="badge badge--category">
{t.topic_category} {t.topic_category}
</span> </span>
{t.topic_tags && t.topic_tags.length > 0 && (
<span className="creator-technique-card__tags">
<TagList tags={t.topic_tags} />
</span>
)}
</span> </span>
{t.summary && (
<span className="creator-technique-card__summary">
{t.summary.length > 120
? `${t.summary.slice(0, 120)}`
: t.summary}
</span>
)}
</Link> </Link>
))} ))}
</div> </div>