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:
parent
0fc0df1d29
commit
da29a2a723
8 changed files with 624 additions and 31 deletions
|
|
@ -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`
|
||||
|
|
|
|||
9
.gsd/milestones/M020/slices/S02/tasks/T01-VERIFY.json
Normal file
9
.gsd/milestones/M020/slices/S02/tasks/T01-VERIFY.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"taskId": "T01",
|
||||
"unitId": "M020/S02/T01",
|
||||
"timestamp": 1775261359844,
|
||||
"passed": true,
|
||||
"discoverySource": "none",
|
||||
"checks": []
|
||||
}
|
||||
83
.gsd/milestones/M020/slices/S02/tasks/T02-SUMMARY.md
Normal file
83
.gsd/milestones/M020/slices/S02/tasks/T02-SUMMARY.md
Normal 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.
|
||||
32
frontend/src/api/creator-dashboard.ts
Normal file
32
frontend/src/api/creator-dashboard.ts
Normal 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`);
|
||||
}
|
||||
|
|
@ -13,3 +13,4 @@ export * from "./reports";
|
|||
export * from "./admin-pipeline";
|
||||
export * from "./admin-techniques";
|
||||
export * from "./auth";
|
||||
export * from "./creator-dashboard";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
Loading…
Add table
Reference in a new issue