Files
full_encoach_platform/frontend/src/pages/PrivacyCenter.tsx
Yamen Ahmad e70a2854f4 feat(frontend): Phase 2/3 hardening release
Roadmap P0
- Ship /logo.svg fallback and rewire asset references (superseded later by
  the project-manager PNG in a separate commit).

Roadmap P1
- Response envelope alignment ({items,total,page,size}) across services
  and query hooks.
- Approval/ticket UX updates tied to the new backend rollback semantics.

Roadmap P2
- React.lazy + Vite manualChunks to split vendor-radix/charts/icons/forms
  from the main bundle and cut initial JS.
- Fix the 181 tsc errors (PaginationParams class + per-service generics).
- Auto-refresh flow for JWT refresh tokens wired into api-client.ts.

Roadmap P3
- i18n: i18next + language detector, en/ar locales, RTL auto-switch,
  LanguageToggle component in RoleLayout and AdminLmsLayout.
- GDPR: PrivacyCenter page for data export and right-to-erasure, backed
  by gdpr.service.ts; logout via AuthContext after erasure.
- Human-in-the-loop exam review UI: admin ExamReviewQueue + ExamReviewDetail
  pages, useExamReview hooks, exam-review.service.ts.
- AI quality loop: AIPromptEditor (versioning + render preview),
  AIFeedbackButtons for students, AIFeedbackTriage admin page, dedicated
  services, hooks, and types.
- Dark mode: ThemeProvider + ThemeToggle + chart color tokens.
- Accessibility: DialogDescription on every DialogContent; alert-dialog
  and sheet updates.
- Retire dead pages: ExamPage marketing, Index, ProfilePage import.
- Playwright e2e scaffolding: playwright.config.ts + e2e/login.spec.ts
  smoke tests, npm scripts test:e2e / test:e2e:install.

Made-with: Cursor
2026-04-19 14:16:32 +04:00

174 lines
5.7 KiB
TypeScript

import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { Download, ShieldAlert, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { gdprService } from "@/services/gdpr.service";
import { useAuth } from "@/contexts/AuthContext";
export default function PrivacyCenter() {
const navigate = useNavigate();
const { logout } = useAuth();
const [exporting, setExporting] = useState(false);
const [erasing, setErasing] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const [confirmText, setConfirmText] = useState("");
const downloadExport = async () => {
try {
setExporting(true);
const payload = await gdprService.exportMyData();
const blob = new Blob([JSON.stringify(payload, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `encoach-data-export-${new Date()
.toISOString()
.slice(0, 10)}.json`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
toast.success("Data export downloaded");
} catch (err) {
toast.error(err instanceof Error ? err.message : "Export failed");
} finally {
setExporting(false);
}
};
const eraseAccount = async () => {
try {
setErasing(true);
await gdprService.eraseMyAccount();
toast.success("Your account has been erased. Signing out…");
setTimeout(async () => {
try {
await logout();
} catch {
// ignore — the server may have already invalidated our tokens
}
navigate("/login", { replace: true });
}, 1200);
} catch (err) {
toast.error(err instanceof Error ? err.message : "Erasure failed");
setErasing(false);
}
};
return (
<div className="mx-auto max-w-3xl space-y-6 p-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">
<ShieldAlert className="mr-1 inline h-5 w-5" />
Privacy Center
</h1>
<p className="text-muted-foreground mt-1 text-sm">
Manage your personal data held by EnCoach under GDPR and equivalent
regulations.
</p>
</div>
<Card>
<CardHeader>
<CardTitle>Download your data</CardTitle>
<CardDescription>
We'll package your profile, entity memberships, exam attempts,
answers, AI feedback, and tickets into a single JSON file.
</CardDescription>
</CardHeader>
<CardContent>
<Button onClick={downloadExport} disabled={exporting}>
<Download className="mr-1 h-4 w-4" />
{exporting ? "Preparing export…" : "Download my data"}
</Button>
</CardContent>
</Card>
<Card className="border-destructive/40">
<CardHeader>
<CardTitle className="text-destructive">Delete my account</CardTitle>
<CardDescription>
This anonymises your profile and removes personal fields from our
records. Exam attempts are retained (without identifying
information) for statistical integrity. This action cannot be
undone.
</CardDescription>
</CardHeader>
<CardContent>
<Button
variant="destructive"
onClick={() => setConfirmOpen(true)}
disabled={erasing}
>
<Trash2 className="mr-1 h-4 w-4" />
{erasing ? "Erasing…" : "Erase my account"}
</Button>
</CardContent>
</Card>
<AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
<AlertDialogContent description="Erasure is permanent — type DELETE to confirm.">
<AlertDialogHeader>
<AlertDialogTitle>Erase your account?</AlertDialogTitle>
<AlertDialogDescription>
We'll anonymise your personal data and deactivate your login.
Aggregate analytics (exam scores, attempt counts) will remain
but will no longer be linked to you.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="space-y-1">
<Label htmlFor="confirm-delete">
Type <strong>DELETE</strong> to confirm:
</Label>
<Input
id="confirm-delete"
value={confirmText}
onChange={(e) => setConfirmText(e.target.value)}
placeholder="DELETE"
/>
</div>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setConfirmText("")}>
Cancel
</AlertDialogCancel>
<AlertDialogAction
disabled={confirmText !== "DELETE" || erasing}
onClick={async () => {
setConfirmOpen(false);
await eraseAccount();
setConfirmText("");
}}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
Erase account
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}