feat: Created TypeScript consent API client with 5 fetch functions and…

- "frontend/src/api/consent.ts"
- "frontend/src/components/ToggleSwitch.tsx"
- "frontend/src/components/ToggleSwitch.module.css"
- "frontend/src/api/index.ts"

GSD-Task: S03/T01
This commit is contained in:
jlightner 2026-04-04 00:21:13 +00:00
parent 2e7fa224bc
commit 2162385c6b
4 changed files with 194 additions and 0 deletions

View file

@ -0,0 +1,78 @@
/**
* Consent API client per-video consent settings and audit history.
*/
import { request, BASE } from "./client";
// ── Types ────────────────────────────────────────────────────────────────────
export interface VideoConsentRead {
source_video_id: string;
video_filename: string;
creator_id: string;
kb_inclusion: boolean;
training_usage: boolean;
public_display: boolean;
updated_at: string;
}
export interface VideoConsentUpdate {
kb_inclusion?: boolean;
training_usage?: boolean;
public_display?: boolean;
}
export interface ConsentAuditEntry {
version: number;
field_name: string;
old_value: boolean | null;
new_value: boolean;
changed_by: string;
created_at: string;
}
export interface ConsentListResponse {
items: VideoConsentRead[];
total: number;
}
export interface ConsentSummary {
total_videos: number;
kb_inclusion_granted: number;
training_usage_granted: number;
public_display_granted: number;
}
// ── Functions ────────────────────────────────────────────────────────────────
export async function fetchConsentList(): Promise<ConsentListResponse> {
return request<ConsentListResponse>(`${BASE}/consent/videos`);
}
export async function fetchVideoConsent(
videoId: string,
): Promise<VideoConsentRead> {
return request<VideoConsentRead>(`${BASE}/consent/videos/${videoId}`);
}
export async function updateVideoConsent(
videoId: string,
updates: VideoConsentUpdate,
): Promise<VideoConsentRead> {
return request<VideoConsentRead>(`${BASE}/consent/videos/${videoId}`, {
method: "PUT",
body: JSON.stringify(updates),
});
}
export async function fetchConsentHistory(
videoId: string,
): Promise<ConsentAuditEntry[]> {
return request<ConsentAuditEntry[]>(
`${BASE}/consent/videos/${videoId}/history`,
);
}
export async function fetchConsentSummary(): Promise<ConsentSummary> {
return request<ConsentSummary>(`${BASE}/consent/summary`);
}

View file

@ -14,3 +14,4 @@ export * from "./admin-pipeline";
export * from "./admin-techniques";
export * from "./auth";
export * from "./creator-dashboard";
export * from "./consent";

View file

@ -0,0 +1,71 @@
/* ToggleSwitch — sliding toggle with accessible checkbox */
.wrapper {
display: inline-flex;
align-items: center;
gap: 0.625rem;
cursor: pointer;
user-select: none;
}
.wrapper[data-disabled] {
opacity: 0.45;
cursor: not-allowed;
pointer-events: none;
}
.label {
font-size: 0.875rem;
color: var(--color-text-primary);
}
/* Track (pill shape) */
.track {
position: relative;
display: inline-flex;
align-items: center;
width: 40px;
height: 22px;
border-radius: 11px;
background: var(--color-border);
transition: background 200ms ease;
flex-shrink: 0;
}
.track[data-checked] {
background: var(--color-accent);
}
/* Visually hidden native checkbox */
.input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Sliding thumb */
.thumb {
position: absolute;
left: 2px;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--color-text-primary);
transition: transform 200ms ease;
pointer-events: none;
}
.track[data-checked] .thumb {
transform: translateX(18px);
}
/* Focus ring on the track when input is focused */
.input:focus-visible ~ .thumb {
box-shadow: 0 0 0 2px var(--color-accent-focus);
}

View file

@ -0,0 +1,44 @@
/**
* Reusable toggle switch accessible checkbox styled as a sliding toggle.
*/
import { useId } from "react";
import styles from "./ToggleSwitch.module.css";
export interface ToggleSwitchProps {
checked: boolean;
onChange: (checked: boolean) => void;
label: string;
disabled?: boolean;
id?: string;
}
export function ToggleSwitch({
checked,
onChange,
label,
disabled = false,
id,
}: ToggleSwitchProps) {
const autoId = useId();
const inputId = id ?? autoId;
return (
<label className={styles.wrapper} htmlFor={inputId} data-disabled={disabled || undefined}>
<span className={styles.label}>{label}</span>
<span className={styles.track} data-checked={checked || undefined}>
<input
id={inputId}
type="checkbox"
role="switch"
className={styles.input}
checked={checked}
disabled={disabled}
aria-label={label}
onChange={(e) => onChange(e.target.checked)}
/>
<span className={styles.thumb} />
</span>
</label>
);
}