/** * ConsentDashboard — per-video consent toggle page for creators. * * Shows all videos with three consent toggles each (KB inclusion, * Training usage, Public display). Each card has an expandable * audit history section loaded on first expand. */ import { useEffect, useState, useCallback } from "react"; import { SidebarNav } from "./CreatorDashboard"; import dashStyles from "./CreatorDashboard.module.css"; import styles from "./ConsentDashboard.module.css"; import { useAuth } from "../context/AuthContext"; import { useDocumentTitle } from "../hooks/useDocumentTitle"; import { ToggleSwitch } from "../components/ToggleSwitch"; import { fetchConsentList, updateVideoConsent, fetchConsentHistory, type VideoConsentRead, type ConsentAuditEntry, } from "../api/consent"; import { ApiError } from "../api/client"; // ── Types ──────────────────────────────────────────────────────────────────── type ConsentField = "kb_inclusion" | "training_usage" | "public_display"; interface VideoCardState { consent: VideoConsentRead; historyOpen: boolean; historyLoaded: boolean; historyLoading: boolean; history: ConsentAuditEntry[]; updating: ConsentField | null; updateError: string | null; } // ── Field labels ───────────────────────────────────────────────────────────── const CONSENT_FIELDS: { key: ConsentField; label: string }[] = [ { key: "kb_inclusion", label: "Knowledge Base Inclusion" }, { key: "training_usage", label: "AI Training Usage" }, { key: "public_display", label: "Public Display" }, ]; // ── Main component ─────────────────────────────────────────────────────────── export default function ConsentDashboard() { useDocumentTitle("Consent Settings"); const { user: _user } = useAuth(); const [cards, setCards] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // ── Fetch consent list on mount ────────────────────────────────────────── useEffect(() => { let cancelled = false; setLoading(true); setError(null); fetchConsentList() .then((res) => { if (cancelled) return; setCards( res.items.map((consent) => ({ consent, historyOpen: false, historyLoaded: false, historyLoading: false, history: [], updating: null, updateError: null, })), ); }) .catch((err) => { if (cancelled) return; setError( err instanceof ApiError ? err.detail : "Failed to load consent settings", ); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, []); // ── Toggle handler ──────────────────────────────────────────────────────── const handleToggle = useCallback( async (videoId: string, field: ConsentField, newValue: boolean) => { // Optimistic update setCards((prev) => prev.map((c) => c.consent.source_video_id === videoId ? { ...c, consent: { ...c.consent, [field]: newValue }, updating: field, updateError: null, } : c, ), ); try { const updated = await updateVideoConsent(videoId, { [field]: newValue, }); setCards((prev) => prev.map((c) => c.consent.source_video_id === videoId ? { ...c, consent: updated, updating: null } : c, ), ); } catch (err) { // Revert optimistic update setCards((prev) => prev.map((c) => c.consent.source_video_id === videoId ? { ...c, consent: { ...c.consent, [field]: !newValue }, updating: null, updateError: err instanceof ApiError ? err.detail : "Update failed — try again", } : c, ), ); } }, [], ); // ── History expand handler ──────────────────────────────────────────────── const toggleHistory = useCallback(async (videoId: string) => { setCards((prev) => prev.map((c) => { if (c.consent.source_video_id !== videoId) return c; if (c.historyOpen) return { ...c, historyOpen: false }; // Opening — need to load if not yet loaded if (c.historyLoaded) return { ...c, historyOpen: true }; return { ...c, historyOpen: true, historyLoading: true }; }), ); // Check if we need to fetch const card = cards.find((c) => c.consent.source_video_id === videoId); if (!card || card.historyLoaded) return; try { const history = await fetchConsentHistory(videoId); setCards((prev) => prev.map((c) => c.consent.source_video_id === videoId ? { ...c, history, historyLoaded: true, historyLoading: false } : c, ), ); } catch { setCards((prev) => prev.map((c) => c.consent.source_video_id === videoId ? { ...c, historyLoading: false, history: [], historyLoaded: true, } : c, ), ); } }, [cards]); // ── Render ──────────────────────────────────────────────────────────────── return (

Consent Settings

{loading && (
Loading consent data…
)} {!loading && error &&
{error}
} {!loading && !error && cards.length === 0 && (

No Videos

No videos found for your creator profile. Upload content to manage consent settings.

)} {!loading && !error && cards.map((card) => ( ))}
); } // ── Video card ─────────────────────────────────────────────────────────────── function VideoConsentCard({ card, onToggle, onToggleHistory, }: { card: VideoCardState; onToggle: ( videoId: string, field: ConsentField, newValue: boolean, ) => void; onToggleHistory: (videoId: string) => void; }) { const { consent, historyOpen, historyLoading, history, updating, updateError } = card; const videoId = consent.source_video_id; return (

{consent.video_filename}

{CONSENT_FIELDS.map(({ key, label }) => (
onToggle(videoId, key, val)} label={label} disabled={updating === key} />
))}
{updateError &&

{updateError}

} {historyOpen && ( <> {historyLoading && (

Loading history…

)} {!historyLoading && history.length === 0 && (

No changes recorded yet.

)} {!historyLoading && history.length > 0 && (
    {history.map((entry, i) => (
  • {entry.field_name} {entry.old_value === null ? "set to" : entry.old_value ? "on → off" : "off → on"} by {entry.changed_by} {new Date(entry.created_at).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", })}
  • ))}
)} )}
); }