feat: Wired ChapterReview into App routes (/creator/chapters, /creator/…

- "frontend/src/App.tsx"
- "frontend/src/pages/CreatorDashboard.tsx"
- "frontend/src/pages/ChapterReview.tsx"
- "frontend/src/pages/ChapterReview.module.css"

GSD-Task: S06/T03
This commit is contained in:
jlightner 2026-04-04 06:12:10 +00:00
parent fa972a4fbc
commit 5be499d0ad
4 changed files with 126 additions and 8 deletions

View file

@ -199,6 +199,7 @@ function AppShell() {
<Route path="/creator/dashboard" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><CreatorDashboard /></Suspense></ProtectedRoute>} />
<Route path="/creator/consent" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><ConsentDashboard /></Suspense></ProtectedRoute>} />
<Route path="/creator/settings" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><CreatorSettings /></Suspense></ProtectedRoute>} />
<Route path="/creator/chapters" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><ChapterReview /></Suspense></ProtectedRoute>} />
<Route path="/creator/chapters/:videoId" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><ChapterReview /></Suspense></ProtectedRoute>} />
{/* Fallback */}

View file

@ -285,3 +285,51 @@
align-items: flex-start;
}
}
/* ── Video Picker ─────────────────────────────────────────────────────────── */
.videoPickerList {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 1rem;
}
.videoPickerCard {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.875rem 1rem;
border-radius: 8px;
background: var(--surface-2, #1e293b);
border: 1px solid var(--border, #334155);
color: var(--text-primary, #e2e8f0);
text-decoration: none;
transition: background 0.15s, border-color 0.15s;
}
.videoPickerCard:hover {
background: var(--surface-3, #2d3a4d);
border-color: var(--accent, #22d3ee);
}
.videoPickerIcon {
width: 20px;
height: 20px;
flex-shrink: 0;
color: var(--text-secondary, #94a3b8);
}
.videoPickerInfo {
display: flex;
flex-direction: column;
min-width: 0;
}
.videoPickerName {
font-size: 0.9rem;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View file

@ -1,5 +1,5 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useParams } from "react-router-dom";
import { Link, useParams } from "react-router-dom";
import WaveSurfer from "wavesurfer.js";
import RegionsPlugin from "wavesurfer.js/dist/plugins/regions.esm.js";
import { SidebarNav } from "./CreatorDashboard";
@ -14,6 +14,7 @@ import {
type ChapterPatch,
type VideoDetail,
} from "../api/videos";
import { fetchConsentList, type VideoConsentRead } from "../api/consent";
import { useDocumentTitle } from "../hooks/useDocumentTitle";
import styles from "./ChapterReview.module.css";
@ -49,12 +50,84 @@ function regionColor(status: string): string {
}
}
/* ── Video Picker (shown when no videoId in URL) ───────────────────────────── */
function VideoPicker() {
const [videos, setVideos] = useState<VideoConsentRead[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
fetchConsentList()
.then((res) => {
if (!cancelled) setVideos(res.items);
})
.catch((err) => {
if (!cancelled)
setError(err instanceof ApiError ? err.detail : "Failed to load videos");
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => { cancelled = true; };
}, []);
return (
<div className={styles.layout}>
<SidebarNav />
<div className={styles.content}>
<h1 className={styles.pageTitle}>Chapter Review</h1>
<p className={styles.subtitle}>Select a video to review its chapters</p>
{loading && <div className={styles.loadingState}>Loading videos</div>}
{!loading && error && <div className={styles.errorState}>{error}</div>}
{!loading && !error && videos.length === 0 && (
<div className={styles.emptyState}>
<h2>No Videos</h2>
<p>You don't have any videos yet.</p>
</div>
)}
{!loading && !error && videos.length > 0 && (
<div className={styles.videoPickerList}>
{videos.map((v) => (
<Link
key={v.source_video_id}
to={`/creator/chapters/${v.source_video_id}`}
className={styles.videoPickerCard}
>
<svg className={styles.videoPickerIcon} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polygon points="23 7 16 12 23 17 23 7" />
<rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
</svg>
<div className={styles.videoPickerInfo}>
<span className={styles.videoPickerName}>{v.video_filename}</span>
</div>
</Link>
))}
</div>
)}
</div>
</div>
);
}
/* ── Main component ────────────────────────────────────────────────────────── */
export default function ChapterReview() {
const { videoId } = useParams<{ videoId: string }>();
useDocumentTitle("Chapter Review");
// If no videoId in URL, show the video picker
if (!videoId) return <VideoPicker />;
return <ChapterReviewDetail videoId={videoId} />;
}
/* ── Detail view (has a videoId) ───────────────────────────────────────────── */
function ChapterReviewDetail({ videoId }: { videoId: string }) {
const [chapters, setChapters] = useState<Chapter[]>([]);
const [video, setVideo] = useState<VideoDetail | null>(null);
const [loading, setLoading] = useState(true);

View file

@ -24,17 +24,13 @@ function SidebarNav() {
</svg>
Dashboard
</NavLink>
<span
className={`${styles.sidebarLink} ${styles.sidebarLinkDisabled}`}
title="Coming soon"
aria-disabled="true"
>
<NavLink to="/creator/chapters" className={linkClass}>
<svg className={styles.sidebarIcon} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
</svg>
Content
</span>
Chapters
</NavLink>
<NavLink to="/creator/consent" className={linkClass}>
<svg className={styles.sidebarIcon} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />