feat: Replaced 3 placeholder cards with real creator dashboard: 4 stat…

- "frontend/src/api/creator-dashboard.ts"
- "frontend/src/pages/CreatorDashboard.tsx"
- "frontend/src/pages/CreatorDashboard.module.css"
- "frontend/src/api/index.ts"

GSD-Task: S02/T02
This commit is contained in:
jlightner 2026-04-04 00:13:48 +00:00
parent 0fc0df1d29
commit da29a2a723
8 changed files with 624 additions and 31 deletions

View file

@ -31,7 +31,7 @@ Constraints:
- Estimate: 45m
- Files: backend/routers/creator_dashboard.py, backend/schemas.py, backend/main.py
- Verify: curl -s -H 'Authorization: Bearer $TOKEN' http://localhost:8000/api/v1/creator/dashboard | python3 -m json.tool — returns JSON with video_count, technique_count, key_moment_count, search_impressions, techniques (array), videos (array). Unauthenticated request returns 401.
- [ ] **T02: Replace placeholder dashboard with real stats and content library** — Replace the three placeholder cards in CreatorDashboard.tsx with real data from the new endpoint. Add the API module, types, and all frontend rendering.
- [x] **T02: Replaced 3 placeholder cards with real creator dashboard: 4 stat cards, techniques table with category badges, videos table with status badges, loading/error/empty states, and responsive mobile layout** — Replace the three placeholder cards in CreatorDashboard.tsx with real data from the new endpoint. Add the API module, types, and all frontend rendering.
Steps:
1. Create `frontend/src/api/creator-dashboard.ts` with TypeScript types matching the backend response schema and a `fetchCreatorDashboard()` function using the shared `request<T>()` helper from `client.ts`

View file

@ -0,0 +1,9 @@
{
"schemaVersion": 1,
"taskId": "T01",
"unitId": "M020/S02/T01",
"timestamp": 1775261359844,
"passed": true,
"discoverySource": "none",
"checks": []
}

View file

@ -0,0 +1,83 @@
---
id: T02
parent: S02
milestone: M020
provides: []
requires: []
affects: []
key_files: ["frontend/src/api/creator-dashboard.ts", "frontend/src/pages/CreatorDashboard.tsx", "frontend/src/pages/CreatorDashboard.module.css", "frontend/src/api/index.ts"]
key_decisions: ["Used ?? '' fallback for CSS module class lookups to satisfy noUncheckedIndexedAccess", "Separate desktop table and mobile card views toggled via CSS media queries"]
patterns_established: []
drill_down_paths: []
observability_surfaces: []
duration: ""
verification_result: "TypeScript check (tsc --noEmit) passed with zero errors. TypeScript build (tsc -b) passed. Vite production build (npm run build) succeeded — 88 modules transformed, bundle produced."
completed_at: 2026-04-04T00:13:32.094Z
blocker_discovered: false
---
# T02: Replaced 3 placeholder cards with real creator dashboard: 4 stat cards, techniques table with category badges, videos table with status badges, loading/error/empty states, and responsive mobile layout
> Replaced 3 placeholder cards with real creator dashboard: 4 stat cards, techniques table with category badges, videos table with status badges, loading/error/empty states, and responsive mobile layout
## What Happened
---
id: T02
parent: S02
milestone: M020
key_files:
- frontend/src/api/creator-dashboard.ts
- frontend/src/pages/CreatorDashboard.tsx
- frontend/src/pages/CreatorDashboard.module.css
- frontend/src/api/index.ts
key_decisions:
- Used ?? '' fallback for CSS module class lookups to satisfy noUncheckedIndexedAccess
- Separate desktop table and mobile card views toggled via CSS media queries
duration: ""
verification_result: passed
completed_at: 2026-04-04T00:13:32.095Z
blocker_discovered: false
---
# T02: Replaced 3 placeholder cards with real creator dashboard: 4 stat cards, techniques table with category badges, videos table with status badges, loading/error/empty states, and responsive mobile layout
**Replaced 3 placeholder cards with real creator dashboard: 4 stat cards, techniques table with category badges, videos table with status badges, loading/error/empty states, and responsive mobile layout**
## What Happened
Created the API module with TypeScript types matching the backend CreatorDashboardResponse and a fetchCreatorDashboard() function. Rewrote CreatorDashboard.tsx to fetch real data on mount and render 4 stat cards, a techniques table with linked titles and category badges, a videos table with processing status badges, loading skeleton, error state, and not-linked empty state. Updated CSS module with stat card styles, data tables, badge variants for both processing status and topic category, mobile card layout, and responsive breakpoints. Fixed noUncheckedIndexedAccess strict mode issues with ?? fallback on CSS module class lookups.
## Verification
TypeScript check (tsc --noEmit) passed with zero errors. TypeScript build (tsc -b) passed. Vite production build (npm run build) succeeded — 88 modules transformed, bundle produced.
## Verification Evidence
| # | Command | Exit Code | Verdict | Duration |
|---|---------|-----------|---------|----------|
| 1 | `cd frontend && npx tsc --noEmit` | 0 | ✅ pass | 4000ms |
| 2 | `cd frontend && npx tsc -b` | 0 | ✅ pass | 3500ms |
| 3 | `cd frontend && npm run build` | 0 | ✅ pass | 4800ms |
## Deviations
Added ?? '' fallback to CSS module class lookups to satisfy noUncheckedIndexedAccess: true in tsconfig — not anticipated in plan.
## Known Issues
None.
## Files Created/Modified
- `frontend/src/api/creator-dashboard.ts`
- `frontend/src/pages/CreatorDashboard.tsx`
- `frontend/src/pages/CreatorDashboard.module.css`
- `frontend/src/api/index.ts`
## Deviations
Added ?? '' fallback to CSS module class lookups to satisfy noUncheckedIndexedAccess: true in tsconfig — not anticipated in plan.
## Known Issues
None.

View file

@ -0,0 +1,32 @@
import { request, BASE } from "./client";
// ── Types ────────────────────────────────────────────────────────────────────
export interface CreatorDashboardTechnique {
title: string;
slug: string;
topic_category: string;
created_at: string;
key_moment_count: number;
}
export interface CreatorDashboardVideo {
filename: string;
processing_status: string;
created_at: string;
}
export interface CreatorDashboardResponse {
video_count: number;
technique_count: number;
key_moment_count: number;
search_impressions: number;
techniques: CreatorDashboardTechnique[];
videos: CreatorDashboardVideo[];
}
// ── Functions ────────────────────────────────────────────────────────────────
export async function fetchCreatorDashboard(): Promise<CreatorDashboardResponse> {
return request<CreatorDashboardResponse>(`${BASE}/creator/dashboard`);
}

View file

@ -13,3 +13,4 @@ export * from "./reports";
export * from "./admin-pipeline";
export * from "./admin-techniques";
export * from "./auth";
export * from "./creator-dashboard";

View file

@ -76,33 +76,263 @@
color: var(--color-text-primary);
}
.cards {
/* ── Stats row ─────────────────────────────────────────────────────────────── */
.statsRow {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
margin-bottom: 2rem;
}
.card {
.statCard {
background: var(--color-bg-surface);
border: 1px solid var(--color-border);
border-radius: 10px;
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
min-height: 80px;
}
.cardTitle {
font-size: 0.875rem;
.statValue {
font-size: 1.75rem;
font-weight: 700;
color: var(--color-text-primary);
font-variant-numeric: tabular-nums;
line-height: 1.1;
}
.statLabel {
font-size: 0.8125rem;
font-weight: 500;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
}
/* ── Sections ──────────────────────────────────────────────────────────────── */
.section {
margin-bottom: 2rem;
}
.sectionTitle {
font-size: 1rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 0 0 0.75rem;
}
.emptyText {
color: var(--color-text-muted);
font-size: 0.875rem;
}
/* ── Table ─────────────────────────────────────────────────────────────────── */
.tableWrap {
overflow-x: auto;
}
.table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
}
.table th {
text-align: left;
padding: 0.625rem 0.75rem;
font-weight: 600;
color: var(--color-text-muted);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.04em;
border-bottom: 1px solid var(--color-border);
}
.table td {
padding: 0.625rem 0.75rem;
color: var(--color-text-secondary);
border-bottom: 1px solid var(--color-border-subtle, var(--color-border));
}
.table tbody tr:hover {
background: var(--color-bg-surface-hover);
}
.link {
color: var(--color-accent);
text-decoration: none;
font-weight: 500;
}
.link:hover {
text-decoration: underline;
}
.filename {
font-family: var(--font-mono, monospace);
font-size: 0.8125rem;
word-break: break-all;
}
/* ── Badges ────────────────────────────────────────────────────────────────── */
.badge {
display: inline-block;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
}
/* Processing status badges */
.badgeComplete {
background: var(--color-badge-approved-bg);
color: var(--color-badge-approved-text);
}
.badgeProcessing {
background: var(--color-badge-edited-bg);
color: var(--color-badge-edited-text);
}
.badgeError {
background: var(--color-badge-rejected-bg);
color: var(--color-badge-rejected-text);
}
.badgePending {
background: var(--color-badge-pending-bg);
color: var(--color-badge-pending-text);
}
/* Category badges */
.badgeCatSoundDesign {
background: var(--color-badge-cat-sound-design-bg);
color: var(--color-badge-cat-sound-design-text);
}
.badgeCatMixing {
background: var(--color-badge-cat-mixing-bg);
color: var(--color-badge-cat-mixing-text);
}
.badgeCatSynthesis {
background: var(--color-badge-cat-synthesis-bg);
color: var(--color-badge-cat-synthesis-text);
}
.badgeCatArrangement {
background: var(--color-badge-cat-arrangement-bg);
color: var(--color-badge-cat-arrangement-text);
}
.badgeCatWorkflow {
background: var(--color-badge-cat-workflow-bg);
color: var(--color-badge-cat-workflow-text);
}
.badgeCatMastering {
background: var(--color-badge-cat-mastering-bg);
color: var(--color-badge-cat-mastering-text);
}
.badgeCatMusicTheory {
background: var(--color-badge-cat-music-theory-bg);
color: var(--color-badge-cat-music-theory-text);
}
.badgeCatDefault {
background: var(--color-badge-category-bg);
color: var(--color-badge-category-text);
}
/* ── Mobile cards (hidden on desktop, shown on mobile) ─────────────────── */
.mobileCards {
display: none;
}
.mobileCard {
background: var(--color-bg-surface);
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 0.875rem 1rem;
}
.mobileCardMeta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
margin-top: 0.375rem;
font-size: 0.8125rem;
color: var(--color-text-muted);
}
/* ── Empty / error states ──────────────────────────────────────────────────── */
.emptyState {
text-align: center;
padding: 3rem 1rem;
color: var(--color-text-muted);
}
.emptyState h2 {
font-size: 1.125rem;
color: var(--color-text-secondary);
margin: 0 0 0.5rem;
}
.cardBody {
font-size: 0.8125rem;
color: var(--color-text-muted);
line-height: 1.5;
.emptyState p {
font-size: 0.875rem;
margin: 0;
}
/* ── Mobile ────────────────────────────────────────────────────────────────── */
.errorState {
background: var(--color-error-bg, rgba(220, 38, 38, 0.1));
color: var(--color-error, #ef4444);
padding: 1rem;
border-radius: 8px;
font-size: 0.875rem;
}
/* ── Skeleton loading ──────────────────────────────────────────────────────── */
.skeleton {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.skeletonPulse {
background: var(--color-bg-surface);
border: 1px solid var(--color-border);
border-radius: 10px;
animation: pulse 1.5s ease-in-out infinite;
}
.skeletonBlock {
height: 120px;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* ── Responsive ────────────────────────────────────────────────────────────── */
@media (max-width: 1024px) {
.statsRow {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.layout {
@ -129,4 +359,19 @@
.content {
padding: 1.25rem;
}
.statsRow {
grid-template-columns: 1fr;
}
/* Hide table, show mobile cards */
.tableWrap {
display: none;
}
.mobileCards {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
}

View file

@ -1,6 +1,12 @@
import { NavLink } from "react-router-dom";
import { useEffect, useState } from "react";
import { Link, NavLink } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
import { useDocumentTitle } from "../hooks/useDocumentTitle";
import {
fetchCreatorDashboard,
type CreatorDashboardResponse,
} from "../api/creator-dashboard";
import { ApiError } from "../api/client";
import styles from "./CreatorDashboard.module.css";
function SidebarNav() {
@ -42,10 +48,95 @@ function SidebarNav() {
export { SidebarNav };
/* ── Stat card ─────────────────────────────────────────────────────────────── */
function StatCard({ value, label }: { value: number; label: string }) {
return (
<div className={styles.statCard}>
<span className={styles.statValue}>{value.toLocaleString()}</span>
<span className={styles.statLabel}>{label}</span>
</div>
);
}
/* ── Helpers ────────────────────────────────────────────────────────────────── */
function formatDate(iso: string): string {
return new Date(iso).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
});
}
/** Map processing_status to a badge CSS class name (module-scoped). */
function statusBadgeClass(status: string): string {
switch (status.toLowerCase()) {
case "complete":
case "completed":
return styles.badgeComplete ?? "";
case "processing":
return styles.badgeProcessing ?? "";
case "error":
case "failed":
return styles.badgeError ?? "";
default:
return styles.badgePending ?? "";
}
}
/** Map topic_category to a badge CSS class name. */
function categoryBadgeClass(cat: string): string {
const slug = cat.toLowerCase().replace(/[\s_]+/g, "-");
const map: Record<string, string | undefined> = {
"sound-design": styles.badgeCatSoundDesign,
mixing: styles.badgeCatMixing,
synthesis: styles.badgeCatSynthesis,
arrangement: styles.badgeCatArrangement,
workflow: styles.badgeCatWorkflow,
mastering: styles.badgeCatMastering,
"music-theory": styles.badgeCatMusicTheory,
};
return map[slug] ?? styles.badgeCatDefault ?? "";
}
/* ── Main component ────────────────────────────────────────────────────────── */
export default function CreatorDashboard() {
useDocumentTitle("Creator Dashboard");
const { user } = useAuth();
const [data, setData] = useState<CreatorDashboardResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
fetchCreatorDashboard()
.then((res) => {
if (!cancelled) setData(res);
})
.catch((err) => {
if (cancelled) return;
if (err instanceof ApiError && err.status === 404) {
// No creator profile linked — show friendly empty state
setError("not_linked");
} else {
setError(err instanceof ApiError ? err.detail : "Failed to load dashboard");
}
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, []);
return (
<div className={styles.layout}>
<SidebarNav />
@ -53,27 +144,159 @@ export default function CreatorDashboard() {
<h1 className={styles.welcome}>
Welcome back{user?.display_name ? `, ${user.display_name}` : ""}
</h1>
<div className={styles.cards}>
<div className={styles.card}>
<h2 className={styles.cardTitle}>Content Stats</h2>
<p className={styles.cardBody}>
Content analytics coming in M020. You'll see views, engagement, and technique performance here.
</p>
{loading && <DashboardSkeleton />}
{!loading && error === "not_linked" && (
<div className={styles.emptyState}>
<h2>No Creator Profile</h2>
<p>Your account isn't linked to a creator profile yet. Contact an admin to get set up.</p>
</div>
<div className={styles.card}>
<h2 className={styles.cardTitle}>Recent Activity</h2>
<p className={styles.cardBody}>
Activity feed coming soon. Track updates to your technique pages.
</p>
)}
{!loading && error && error !== "not_linked" && (
<div className={styles.errorState}>
<p>Could not load dashboard: {error}</p>
</div>
<div className={styles.card}>
<h2 className={styles.cardTitle}>Quick Actions</h2>
<p className={styles.cardBody}>
Content management tools will appear here once the creator content module is live.
</p>
</div>
</div>
)}
{!loading && !error && data && (
<>
{/* ── Stats row ──────────────────────────────────────────── */}
<div className={styles.statsRow}>
<StatCard value={data.video_count} label="Uploads" />
<StatCard value={data.technique_count} label="Techniques" />
<StatCard value={data.key_moment_count} label="Key Moments" />
<StatCard value={data.search_impressions} label="Search Impressions" />
</div>
{/* ── Techniques ─────────────────────────────────────────── */}
<section className={styles.section}>
<h2 className={styles.sectionTitle}>Technique Pages</h2>
{data.techniques.length === 0 ? (
<p className={styles.emptyText}>No technique pages yet.</p>
) : (
<>
{/* Desktop table */}
<div className={styles.tableWrap}>
<table className={styles.table}>
<thead>
<tr>
<th>Title</th>
<th>Category</th>
<th>Moments</th>
<th>Created</th>
</tr>
</thead>
<tbody>
{data.techniques.map((t) => (
<tr key={t.slug}>
<td>
<Link to={`/techniques/${t.slug}`} className={styles.link}>
{t.title}
</Link>
</td>
<td>
<span className={`${styles.badge} ${categoryBadgeClass(t.topic_category)}`}>
{t.topic_category}
</span>
</td>
<td>{t.key_moment_count}</td>
<td>{formatDate(t.created_at)}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile cards */}
<div className={styles.mobileCards}>
{data.techniques.map((t) => (
<div key={t.slug} className={styles.mobileCard}>
<Link to={`/techniques/${t.slug}`} className={styles.link}>
{t.title}
</Link>
<div className={styles.mobileCardMeta}>
<span className={`${styles.badge} ${categoryBadgeClass(t.topic_category)}`}>
{t.topic_category}
</span>
<span>{t.key_moment_count} moments</span>
<span>{formatDate(t.created_at)}</span>
</div>
</div>
))}
</div>
</>
)}
</section>
{/* ── Videos ──────────────────────────────────────────────── */}
<section className={styles.section}>
<h2 className={styles.sectionTitle}>Source Videos</h2>
{data.videos.length === 0 ? (
<p className={styles.emptyText}>No videos uploaded yet.</p>
) : (
<>
<div className={styles.tableWrap}>
<table className={styles.table}>
<thead>
<tr>
<th>Filename</th>
<th>Status</th>
<th>Uploaded</th>
</tr>
</thead>
<tbody>
{data.videos.map((v) => (
<tr key={v.filename}>
<td className={styles.filename}>{v.filename}</td>
<td>
<span className={`${styles.badge} ${statusBadgeClass(v.processing_status)}`}>
{v.processing_status}
</span>
</td>
<td>{formatDate(v.created_at)}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className={styles.mobileCards}>
{data.videos.map((v) => (
<div key={v.filename} className={styles.mobileCard}>
<span className={styles.filename}>{v.filename}</span>
<div className={styles.mobileCardMeta}>
<span className={`${styles.badge} ${statusBadgeClass(v.processing_status)}`}>
{v.processing_status}
</span>
<span>{formatDate(v.created_at)}</span>
</div>
</div>
))}
</div>
</>
)}
</section>
</>
)}
</div>
</div>
);
}
/* ── Loading skeleton ──────────────────────────────────────────────────────── */
function DashboardSkeleton() {
return (
<div className={styles.skeleton}>
<div className={styles.statsRow}>
{[1, 2, 3, 4].map((i) => (
<div key={i} className={`${styles.statCard} ${styles.skeletonPulse}`} />
))}
</div>
<div className={`${styles.skeletonBlock} ${styles.skeletonPulse}`} />
<div className={`${styles.skeletonBlock} ${styles.skeletonPulse}`} />
</div>
);
}

View file

@ -1 +1 @@
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/admin-pipeline.ts","./src/api/admin-techniques.ts","./src/api/auth.ts","./src/api/client.ts","./src/api/creators.ts","./src/api/index.ts","./src/api/reports.ts","./src/api/search.ts","./src/api/stats.ts","./src/api/techniques.ts","./src/api/topics.ts","./src/api/videos.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/CategoryIcons.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/PlayerControls.tsx","./src/components/ProtectedRoute.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SocialIcons.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/components/TranscriptSidebar.tsx","./src/components/VideoPlayer.tsx","./src/context/AuthContext.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useMediaSync.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/CreatorDashboard.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorSettings.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/Login.tsx","./src/pages/Register.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/pages/WatchPage.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx"],"version":"5.6.3"}
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/admin-pipeline.ts","./src/api/admin-techniques.ts","./src/api/auth.ts","./src/api/client.ts","./src/api/creator-dashboard.ts","./src/api/creators.ts","./src/api/index.ts","./src/api/reports.ts","./src/api/search.ts","./src/api/stats.ts","./src/api/techniques.ts","./src/api/topics.ts","./src/api/videos.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/CategoryIcons.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/PlayerControls.tsx","./src/components/ProtectedRoute.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SocialIcons.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/components/TranscriptSidebar.tsx","./src/components/VideoPlayer.tsx","./src/context/AuthContext.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useMediaSync.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/CreatorDashboard.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorSettings.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/Login.tsx","./src/pages/Register.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/pages/WatchPage.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx"],"version":"5.6.3"}