mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
Phase 4b's `<SectionFrame>` already paints the canonical h1 + subtitle for each `/admin/<slug>` route, but every section card kept its v1.4 in-card h2 with the same (or near-same) text. The result was the section title announcing twice — once as h1, then immediately again as h2 — both visually redundant and a screen-reader landmark anti-pattern. Demoting the in-card label to a `<div>` preserves the visual look (same Tailwind classes) while removing the duplicate heading. The four sub-cards inside `IntegrationsGroupSection` (umami, glitchtip, web-push, bug-report) keep their h2 because they live under the "Integrations" h1 and genuinely deserve a heading level — that is the non-duplicating case. Closes H1 from .planning/phase-6-design-findings.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
130 lines
4.2 KiB
TypeScript
130 lines
4.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { AlertTriangle, Loader2, Trash2 } from "lucide-react";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from "@/components/ui/alert-dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { useTranslations } from "@/lib/i18n/context";
|
|
|
|
export function DangerZoneSection() {
|
|
const { t } = useTranslations();
|
|
const queryClient = useQueryClient();
|
|
const [wipeMsg, setWipeMsg] = useState<string | null>(null);
|
|
|
|
const wipeAllData = useMutation({
|
|
mutationFn: async () => {
|
|
const res = await fetch("/api/admin/data", {
|
|
method: "DELETE",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ confirm: "DELETE ALL" }),
|
|
});
|
|
const json = await res.json();
|
|
if (!res.ok) throw new Error(json.error || t("common.error"));
|
|
return json.data as {
|
|
measurements: number;
|
|
intakeEvents: number;
|
|
medications: number;
|
|
apiTokens: number;
|
|
withingsConnections: number;
|
|
authChallenges: number;
|
|
notificationChannels: number;
|
|
pushSubscriptions: number;
|
|
telegramScheduledDeletions: number;
|
|
};
|
|
},
|
|
onSuccess: (data) => {
|
|
queryClient.invalidateQueries();
|
|
setWipeMsg(
|
|
t("admin.deletedResult", {
|
|
measurements: data.measurements,
|
|
medications: data.medications,
|
|
intakeEvents: data.intakeEvents,
|
|
apiTokens: data.apiTokens,
|
|
withingsConnections: data.withingsConnections,
|
|
authChallenges: data.authChallenges,
|
|
notificationChannels: data.notificationChannels,
|
|
pushSubscriptions: data.pushSubscriptions,
|
|
telegramScheduledDeletions: data.telegramScheduledDeletions,
|
|
}),
|
|
);
|
|
},
|
|
onError: (err: Error) => {
|
|
setWipeMsg(err.message);
|
|
},
|
|
});
|
|
|
|
return (
|
|
<div className="bg-destructive/5 border-destructive/30 rounded-xl border p-6">
|
|
<div className="flex items-center gap-2">
|
|
<AlertTriangle className="text-destructive h-5 w-5" />
|
|
<div className="text-destructive text-lg font-semibold">
|
|
{t("admin.dangerZone")}
|
|
</div>
|
|
</div>
|
|
<div className="mt-4">
|
|
<p className="text-sm font-medium">{t("admin.deleteAllData")}</p>
|
|
<p className="text-muted-foreground text-xs">
|
|
{t("admin.deleteAllDescription")}
|
|
</p>
|
|
<div className="mt-3">
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
disabled={wipeAllData.isPending}
|
|
>
|
|
{wipeAllData.isPending ? (
|
|
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
|
) : (
|
|
<Trash2 className="mr-1 h-3.5 w-3.5" />
|
|
)}
|
|
{t("admin.deleteButton")}
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>
|
|
{t("admin.deleteAllConfirm")}
|
|
</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
{t("admin.deleteAllConfirmDescription")}
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
onClick={() => wipeAllData.mutate()}
|
|
>
|
|
{t("admin.finalDelete")}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
{wipeMsg && (
|
|
<p
|
|
className={`mt-2 text-sm ${
|
|
wipeAllData.isError ? "text-destructive" : "text-dracula-green"
|
|
}`}
|
|
>
|
|
{wipeMsg}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|