From 2e7fa224bc6892bc8611c6975e1487f6d1564999 Mon Sep 17 00:00:00 2001 From: jlightner Date: Sat, 4 Apr 2026 00:13:48 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Replaced=203=20placeholder=20cards=20wi?= =?UTF-8?q?th=20real=20creator=20dashboard:=204=20stat=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "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 --- frontend/src/api/creator-dashboard.ts | 32 +++ frontend/src/api/index.ts | 1 + .../src/pages/CreatorDashboard.module.css | 265 +++++++++++++++++- frontend/src/pages/CreatorDashboard.tsx | 261 +++++++++++++++-- frontend/tsconfig.app.tsbuildinfo | 2 +- 5 files changed, 531 insertions(+), 30 deletions(-) create mode 100644 frontend/src/api/creator-dashboard.ts diff --git a/frontend/src/api/creator-dashboard.ts b/frontend/src/api/creator-dashboard.ts new file mode 100644 index 0000000..e8ea572 --- /dev/null +++ b/frontend/src/api/creator-dashboard.ts @@ -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 { + return request(`${BASE}/creator/dashboard`); +} diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index eb78d5e..a4b2d58 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -13,3 +13,4 @@ export * from "./reports"; export * from "./admin-pipeline"; export * from "./admin-techniques"; export * from "./auth"; +export * from "./creator-dashboard"; diff --git a/frontend/src/pages/CreatorDashboard.module.css b/frontend/src/pages/CreatorDashboard.module.css index f0f4afe..50e1bfc 100644 --- a/frontend/src/pages/CreatorDashboard.module.css +++ b/frontend/src/pages/CreatorDashboard.module.css @@ -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; + } } diff --git a/frontend/src/pages/CreatorDashboard.tsx b/frontend/src/pages/CreatorDashboard.tsx index c580d82..927210b 100644 --- a/frontend/src/pages/CreatorDashboard.tsx +++ b/frontend/src/pages/CreatorDashboard.tsx @@ -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 ( +
+ {value.toLocaleString()} + {label} +
+ ); +} + +/* ── 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 = { + "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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 (
@@ -53,27 +144,159 @@ export default function CreatorDashboard() {

Welcome back{user?.display_name ? `, ${user.display_name}` : ""}

-
-
-

Content Stats

-

- Content analytics coming in M020. You'll see views, engagement, and technique performance here. -

+ + {loading && } + + {!loading && error === "not_linked" && ( +
+

No Creator Profile

+

Your account isn't linked to a creator profile yet. Contact an admin to get set up.

-
-

Recent Activity

-

- Activity feed coming soon. Track updates to your technique pages. -

+ )} + + {!loading && error && error !== "not_linked" && ( +
+

Could not load dashboard: {error}

-
-

Quick Actions

-

- Content management tools will appear here once the creator content module is live. -

-
-
+ )} + + {!loading && !error && data && ( + <> + {/* ── Stats row ──────────────────────────────────────────── */} +
+ + + + +
+ + {/* ── Techniques ─────────────────────────────────────────── */} +
+

Technique Pages

+ {data.techniques.length === 0 ? ( +

No technique pages yet.

+ ) : ( + <> + {/* Desktop table */} +
+ + + + + + + + + + + {data.techniques.map((t) => ( + + + + + + + ))} + +
TitleCategoryMomentsCreated
+ + {t.title} + + + + {t.topic_category} + + {t.key_moment_count}{formatDate(t.created_at)}
+
+ + {/* Mobile cards */} +
+ {data.techniques.map((t) => ( +
+ + {t.title} + +
+ + {t.topic_category} + + {t.key_moment_count} moments + {formatDate(t.created_at)} +
+
+ ))} +
+ + )} +
+ + {/* ── Videos ──────────────────────────────────────────────── */} +
+

Source Videos

+ {data.videos.length === 0 ? ( +

No videos uploaded yet.

+ ) : ( + <> +
+ + + + + + + + + + {data.videos.map((v) => ( + + + + + + ))} + +
FilenameStatusUploaded
{v.filename} + + {v.processing_status} + + {formatDate(v.created_at)}
+
+ +
+ {data.videos.map((v) => ( +
+ {v.filename} +
+ + {v.processing_status} + + {formatDate(v.created_at)} +
+
+ ))} +
+ + )} +
+ + )}
); } + +/* ── Loading skeleton ──────────────────────────────────────────────────────── */ + +function DashboardSkeleton() { + return ( +
+
+ {[1, 2, 3, 4].map((i) => ( +
+ ))} +
+
+
+
+ ); +} diff --git a/frontend/tsconfig.app.tsbuildinfo b/frontend/tsconfig.app.tsbuildinfo index e2a350a..a24504a 100644 --- a/frontend/tsconfig.app.tsbuildinfo +++ b/frontend/tsconfig.app.tsbuildinfo @@ -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"} \ No newline at end of file +{"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"} \ No newline at end of file