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:
parent
2e7fa224bc
commit
2162385c6b
4 changed files with 194 additions and 0 deletions
78
frontend/src/api/consent.ts
Normal file
78
frontend/src/api/consent.ts
Normal 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`);
|
||||||
|
}
|
||||||
|
|
@ -14,3 +14,4 @@ export * from "./admin-pipeline";
|
||||||
export * from "./admin-techniques";
|
export * from "./admin-techniques";
|
||||||
export * from "./auth";
|
export * from "./auth";
|
||||||
export * from "./creator-dashboard";
|
export * from "./creator-dashboard";
|
||||||
|
export * from "./consent";
|
||||||
|
|
|
||||||
71
frontend/src/components/ToggleSwitch.module.css
Normal file
71
frontend/src/components/ToggleSwitch.module.css
Normal 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);
|
||||||
|
}
|
||||||
44
frontend/src/components/ToggleSwitch.tsx
Normal file
44
frontend/src/components/ToggleSwitch.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue