feat: Built ConsentDashboard page with per-video consent toggles, expan…
- "frontend/src/pages/ConsentDashboard.tsx" - "frontend/src/pages/ConsentDashboard.module.css" - "frontend/src/pages/CreatorDashboard.tsx" - "frontend/src/App.tsx" GSD-Task: S03/T02
This commit is contained in:
parent
31638b5a3a
commit
4115c8add0
8 changed files with 597 additions and 2 deletions
|
|
@ -23,7 +23,7 @@
|
|||
- Estimate: 30m
|
||||
- Files: frontend/src/api/consent.ts, frontend/src/components/ToggleSwitch.tsx, frontend/src/components/ToggleSwitch.module.css, frontend/src/api/index.ts
|
||||
- Verify: cd frontend && npx tsc --noEmit && echo 'Types OK'
|
||||
- [ ] **T02: Build ConsentDashboard page with route wiring and sidebar nav** — Build the main consent page, wire it into the app router and sidebar navigation. This is the user-facing deliverable.
|
||||
- [x] **T02: Built ConsentDashboard page with per-video consent toggles, expandable audit history, optimistic updates, and wired it into the router and sidebar navigation** — Build the main consent page, wire it into the app router and sidebar navigation. This is the user-facing deliverable.
|
||||
|
||||
## Steps
|
||||
|
||||
|
|
|
|||
30
.gsd/milestones/M020/slices/S03/tasks/T01-VERIFY.json
Normal file
30
.gsd/milestones/M020/slices/S03/tasks/T01-VERIFY.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"taskId": "T01",
|
||||
"unitId": "M020/S03/T01",
|
||||
"timestamp": 1775262073341,
|
||||
"passed": false,
|
||||
"discoverySource": "task-plan",
|
||||
"checks": [
|
||||
{
|
||||
"command": "cd frontend",
|
||||
"exitCode": 0,
|
||||
"durationMs": 4,
|
||||
"verdict": "pass"
|
||||
},
|
||||
{
|
||||
"command": "npx tsc --noEmit",
|
||||
"exitCode": 1,
|
||||
"durationMs": 804,
|
||||
"verdict": "fail"
|
||||
},
|
||||
{
|
||||
"command": "echo 'Types OK'",
|
||||
"exitCode": 0,
|
||||
"durationMs": 6,
|
||||
"verdict": "pass"
|
||||
}
|
||||
],
|
||||
"retryAttempt": 1,
|
||||
"maxRetries": 2
|
||||
}
|
||||
83
.gsd/milestones/M020/slices/S03/tasks/T02-SUMMARY.md
Normal file
83
.gsd/milestones/M020/slices/S03/tasks/T02-SUMMARY.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
---
|
||||
id: T02
|
||||
parent: S03
|
||||
milestone: M020
|
||||
provides: []
|
||||
requires: []
|
||||
affects: []
|
||||
key_files: ["frontend/src/pages/ConsentDashboard.tsx", "frontend/src/pages/ConsentDashboard.module.css", "frontend/src/pages/CreatorDashboard.tsx", "frontend/src/App.tsx"]
|
||||
key_decisions: ["Used padlock SVG icon for Consent sidebar link", "Stored per-card state in single cards array for simplicity", "Optimistic toggle update with revert on API error"]
|
||||
patterns_established: []
|
||||
drill_down_paths: []
|
||||
observability_surfaces: []
|
||||
duration: ""
|
||||
verification_result: "TypeScript type checking (tsc --noEmit) and Vite production build (npm run build) both pass with exit code 0. ConsentDashboard is code-split into its own chunk."
|
||||
completed_at: 2026-04-04T00:24:14.390Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T02: Built ConsentDashboard page with per-video consent toggles, expandable audit history, optimistic updates, and wired it into the router and sidebar navigation
|
||||
|
||||
> Built ConsentDashboard page with per-video consent toggles, expandable audit history, optimistic updates, and wired it into the router and sidebar navigation
|
||||
|
||||
## What Happened
|
||||
---
|
||||
id: T02
|
||||
parent: S03
|
||||
milestone: M020
|
||||
key_files:
|
||||
- frontend/src/pages/ConsentDashboard.tsx
|
||||
- frontend/src/pages/ConsentDashboard.module.css
|
||||
- frontend/src/pages/CreatorDashboard.tsx
|
||||
- frontend/src/App.tsx
|
||||
key_decisions:
|
||||
- Used padlock SVG icon for Consent sidebar link
|
||||
- Stored per-card state in single cards array for simplicity
|
||||
- Optimistic toggle update with revert on API error
|
||||
duration: ""
|
||||
verification_result: passed
|
||||
completed_at: 2026-04-04T00:24:14.390Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T02: Built ConsentDashboard page with per-video consent toggles, expandable audit history, optimistic updates, and wired it into the router and sidebar navigation
|
||||
|
||||
**Built ConsentDashboard page with per-video consent toggles, expandable audit history, optimistic updates, and wired it into the router and sidebar navigation**
|
||||
|
||||
## What Happened
|
||||
|
||||
Created the ConsentDashboard page component with full state management for video consent toggles. Each video renders as a card with three ToggleSwitch components (KB Inclusion, AI Training Usage, Public Display). Toggles use optimistic updates with revert on API error. Each card has a lazy-loaded audit history section. Added padlock icon to sidebar nav and wired the route in App.tsx with ProtectedRoute + Suspense.
|
||||
|
||||
## Verification
|
||||
|
||||
TypeScript type checking (tsc --noEmit) and Vite production build (npm run build) both pass with exit code 0. ConsentDashboard is code-split into its own chunk.
|
||||
|
||||
## Verification Evidence
|
||||
|
||||
| # | Command | Exit Code | Verdict | Duration |
|
||||
|---|---------|-----------|---------|----------|
|
||||
| 1 | `npx tsc --noEmit` | 0 | ✅ pass | 3000ms |
|
||||
| 2 | `npm run build` | 0 | ✅ pass | 2000ms |
|
||||
|
||||
|
||||
## Deviations
|
||||
|
||||
None.
|
||||
|
||||
## Known Issues
|
||||
|
||||
None.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `frontend/src/pages/ConsentDashboard.tsx`
|
||||
- `frontend/src/pages/ConsentDashboard.module.css`
|
||||
- `frontend/src/pages/CreatorDashboard.tsx`
|
||||
- `frontend/src/App.tsx`
|
||||
|
||||
|
||||
## Deviations
|
||||
None.
|
||||
|
||||
## Known Issues
|
||||
None.
|
||||
|
|
@ -17,6 +17,7 @@ const AdminTechniquePages = React.lazy(() => import("./pages/AdminTechniquePages
|
|||
const About = React.lazy(() => import("./pages/About"));
|
||||
const CreatorDashboard = React.lazy(() => import("./pages/CreatorDashboard"));
|
||||
const CreatorSettings = React.lazy(() => import("./pages/CreatorSettings"));
|
||||
const ConsentDashboard = React.lazy(() => import("./pages/ConsentDashboard"));
|
||||
const WatchPage = React.lazy(() => import("./pages/WatchPage"));
|
||||
import AdminDropdown from "./components/AdminDropdown";
|
||||
import AppFooter from "./components/AppFooter";
|
||||
|
|
@ -188,6 +189,7 @@ function AppShell() {
|
|||
|
||||
{/* Creator routes (protected) */}
|
||||
<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>} />
|
||||
|
||||
{/* Fallback */}
|
||||
|
|
|
|||
144
frontend/src/pages/ConsentDashboard.module.css
Normal file
144
frontend/src/pages/ConsentDashboard.module.css
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
/* ── Consent Dashboard ─────────────────────────────────────────────────────── */
|
||||
|
||||
.heading {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
/* ── Video consent cards ───────────────────────────────────────────────────── */
|
||||
|
||||
.videoRow {
|
||||
background: var(--color-bg-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
padding: 1.25rem 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.videoTitle {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.toggleGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.toggleRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.updateError {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
/* ── Audit history ─────────────────────────────────────────────────────────── */
|
||||
|
||||
.historyToggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
margin-top: 0.75rem;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.historyToggle:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.historyArrow {
|
||||
display: inline-block;
|
||||
transition: transform 0.2s;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.historyArrowOpen {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.historyList {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.historyEntry {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
align-items: baseline;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-secondary);
|
||||
padding: 0.375rem 0.5rem;
|
||||
background: var(--color-bg-input, rgba(255, 255, 255, 0.03));
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.historyField {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.historyDate {
|
||||
margin-left: auto;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* ── States ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 3rem 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #f87171;
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.emptyState h2 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.emptyState p {
|
||||
margin: 0;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
329
frontend/src/pages/ConsentDashboard.tsx
Normal file
329
frontend/src/pages/ConsentDashboard.tsx
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
/**
|
||||
* 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<VideoCardState[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div className={dashStyles.layout}>
|
||||
<SidebarNav />
|
||||
<div className={dashStyles.content}>
|
||||
<h1 className={styles.heading}>Consent Settings</h1>
|
||||
|
||||
{loading && (
|
||||
<div className={styles.loading}>Loading consent data…</div>
|
||||
)}
|
||||
|
||||
{!loading && error && <div className={styles.error}>{error}</div>}
|
||||
|
||||
{!loading && !error && cards.length === 0 && (
|
||||
<div className={styles.emptyState}>
|
||||
<h2>No Videos</h2>
|
||||
<p>
|
||||
No videos found for your creator profile. Upload content to manage
|
||||
consent settings.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading &&
|
||||
!error &&
|
||||
cards.map((card) => (
|
||||
<VideoConsentCard
|
||||
key={card.consent.source_video_id}
|
||||
card={card}
|
||||
onToggle={handleToggle}
|
||||
onToggleHistory={toggleHistory}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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 (
|
||||
<div className={styles.videoRow}>
|
||||
<h3 className={styles.videoTitle}>{consent.video_filename}</h3>
|
||||
|
||||
<div className={styles.toggleGroup}>
|
||||
{CONSENT_FIELDS.map(({ key, label }) => (
|
||||
<div className={styles.toggleRow} key={key}>
|
||||
<ToggleSwitch
|
||||
checked={consent[key]}
|
||||
onChange={(val) => onToggle(videoId, key, val)}
|
||||
label={label}
|
||||
disabled={updating === key}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{updateError && <p className={styles.updateError}>{updateError}</p>}
|
||||
|
||||
<button
|
||||
className={styles.historyToggle}
|
||||
onClick={() => onToggleHistory(videoId)}
|
||||
aria-expanded={historyOpen}
|
||||
>
|
||||
<span
|
||||
className={`${styles.historyArrow} ${historyOpen ? styles.historyArrowOpen : ""}`}
|
||||
>
|
||||
▸
|
||||
</span>
|
||||
{historyOpen ? "Hide history" : "Show history"}
|
||||
</button>
|
||||
|
||||
{historyOpen && (
|
||||
<>
|
||||
{historyLoading && (
|
||||
<p className={styles.loading} style={{ padding: "0.5rem 0" }}>
|
||||
Loading history…
|
||||
</p>
|
||||
)}
|
||||
{!historyLoading && history.length === 0 && (
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.8125rem",
|
||||
color: "var(--color-text-secondary)",
|
||||
marginTop: "0.5rem",
|
||||
}}
|
||||
>
|
||||
No changes recorded yet.
|
||||
</p>
|
||||
)}
|
||||
{!historyLoading && history.length > 0 && (
|
||||
<ul className={styles.historyList}>
|
||||
{history.map((entry, i) => (
|
||||
<li key={i} className={styles.historyEntry}>
|
||||
<span className={styles.historyField}>
|
||||
{entry.field_name}
|
||||
</span>
|
||||
<span>
|
||||
{entry.old_value === null
|
||||
? "set to"
|
||||
: entry.old_value
|
||||
? "on → off"
|
||||
: "off → on"}
|
||||
</span>
|
||||
<span>by {entry.changed_by}</span>
|
||||
<span className={styles.historyDate}>
|
||||
{new Date(entry.created_at).toLocaleDateString(undefined, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -35,6 +35,13 @@ function SidebarNav() {
|
|||
</svg>
|
||||
Content
|
||||
</span>
|
||||
<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" />
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
||||
</svg>
|
||||
Consent
|
||||
</NavLink>
|
||||
<NavLink to="/creator/settings" className={linkClass}>
|
||||
<svg className={styles.sidebarIcon} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
|
|
|
|||
|
|
@ -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/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"}
|
||||
{"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/consent.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/ToggleSwitch.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/ConsentDashboard.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