Files
HealthLog/src/app/bugreport/page.tsx
T
Marc-André Bombeck 32a023dfff chore(ui): apply M7 + L1 + L3 + L4 polish across medication form, motion-reduce, details aria, therapy timeline
W10 design-review carry-over items. M7 lifts the medication-form
day-of-week buttons, the more-horizontal trigger, and the schedule
label-input gaps to the 44 px / space-y-2 standard used by every
other settings surface. L1 pairs every animate-spin/pulse/bounce
with motion-reduce:animate-none across the component tree so users
with prefers-reduced-motion get a still surface. L3 adds the
aria-controls + panel-id pairing to the inventory + coach-evidence
<details> blocks for parity with the Health Score provenance
accordion. L4 inserts a sr-only h4 per therapy-timeline entry so
screen-reader users can jump between medication blocks.
2026-05-15 17:13:42 +02:00

241 lines
7.8 KiB
TypeScript

"use client";
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Loader2, CheckCircle2, Bug, GitPullRequest, Info } from "lucide-react";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
interface BugReportStatus {
configured: boolean;
enabled: boolean;
isAdmin: boolean;
}
const CATEGORIES = [
{ value: "BUG", labelKey: "bugreport.categoryBug" },
{ value: "FEATURE_REQUEST", labelKey: "bugreport.categoryFeature" },
{ value: "QUESTION", labelKey: "bugreport.categoryQuestion" },
{ value: "OTHER", labelKey: "bugreport.categoryOther" },
] as const;
type CategoryValue = (typeof CATEGORIES)[number]["value"];
export default function BugReportPage() {
const { isAuthenticated } = useAuth();
const { t, locale } = useTranslations();
const [category, setCategory] = useState<CategoryValue>("BUG");
const [subject, setSubject] = useState("");
const [description, setDescription] = useState("");
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<{
type: "success" | "error";
message: string;
} | null>(null);
// Status is now informational only — submission always works.
const { data: status } = useQuery({
queryKey: queryKeys.bugreportStatus(),
queryFn: async () => {
const res = await fetch("/api/bugreport/status");
if (!res.ok) throw new Error("Failed to load status");
const json = await res.json();
return json.data as BugReportStatus;
},
enabled: isAuthenticated,
});
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setResult(null);
setLoading(true);
try {
const res = await fetch("/api/feedback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
category,
subject: subject || description.slice(0, 60),
description,
metadata: {
locale,
userAgent:
typeof navigator !== "undefined" ? navigator.userAgent : null,
url:
typeof window !== "undefined" ? window.location.pathname : null,
},
}),
});
const json = await res.json();
if (res.ok) {
setResult({ type: "success", message: t("bugreport.success") });
setSubject("");
setDescription("");
} else {
setResult({
type: "error",
message: json.error || t("bugreport.errorCreating"),
});
}
} catch {
setResult({ type: "error", message: t("common.networkError") });
} finally {
setLoading(false);
}
}
if (!isAuthenticated) {
return (
<div className="mx-auto w-full max-w-6xl space-y-6">
<h1 className="text-2xl font-bold tracking-tight">
{t("bugreport.title")}
</h1>
<p className="text-muted-foreground text-sm">
{t("bugreport.loginRequired")}
</p>
</div>
);
}
if (status && status.enabled === false) {
return (
<div className="mx-auto w-full max-w-6xl space-y-6">
<div>
<h1 className="text-2xl font-bold tracking-tight">
{t("bugreport.title")}
</h1>
<p className="text-muted-foreground text-sm">
{t("bugreport.subtitle")}
</p>
</div>
<div className="bg-card border-border border-l-dracula-orange flex gap-2 rounded-lg border-l-4 p-3 text-sm">
<Info className="text-dracula-orange mt-0.5 h-4 w-4 shrink-0" />
<div>
<p className="text-foreground font-medium">
{t("bugreport.disabledTitle")}
</p>
<p className="text-muted-foreground mt-1">
{t("bugreport.disabledBody")}
</p>
</div>
</div>
</div>
);
}
return (
<div className="mx-auto w-full max-w-6xl space-y-6">
<div>
<h1 className="text-2xl font-bold tracking-tight">
{t("bugreport.title")}
</h1>
<p className="text-muted-foreground text-sm">
{t("bugreport.subtitle")}
</p>
</div>
{status?.configured && (
<div className="bg-card border-border border-l-dracula-cyan flex gap-2 rounded-lg border-l-4 p-3 text-sm">
<GitPullRequest className="text-dracula-cyan mt-0.5 h-4 w-4 shrink-0" />
<p className="text-muted-foreground">
{t("bugreport.githubEscalationNote")}
</p>
</div>
)}
{!status?.configured && (
<div className="bg-card border-border border-l-dracula-purple flex gap-2 rounded-lg border-l-4 p-3 text-sm">
<Info className="text-dracula-purple mt-0.5 h-4 w-4 shrink-0" />
<p className="text-muted-foreground">
{t("bugreport.internalOnlyNote")}
</p>
</div>
)}
<div className="bg-card border-border w-full rounded-xl border p-6">
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-[200px_1fr]">
<div className="space-y-2">
<Label htmlFor="category">{t("bugreport.category")}</Label>
<Select
value={category}
onValueChange={(v) => setCategory(v as CategoryValue)}
>
<SelectTrigger id="category">
<SelectValue />
</SelectTrigger>
<SelectContent>
{CATEGORIES.map((c) => (
<SelectItem key={c.value} value={c.value}>
{t(c.labelKey)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="subject">{t("bugreport.bugTitle")}</Label>
<Input
id="subject"
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder={t("bugreport.bugTitlePlaceholder")}
maxLength={200}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="bug-desc">{t("bugreport.description")}</Label>
<textarea
id="bug-desc"
value={description}
onChange={(e) => setDescription(e.target.value)}
required
minLength={10}
maxLength={5000}
rows={8}
className="border-input bg-background text-foreground placeholder:text-muted-foreground focus-visible:ring-ring w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:outline-none"
/>
</div>
{result && (
<div
className={`rounded-lg p-3 text-sm ${
result.type === "success"
? "bg-dracula-green/10 text-dracula-green"
: "bg-destructive/10 text-destructive"
}`}
>
{result.type === "success" && (
<CheckCircle2 className="mr-2 inline h-4 w-4" />
)}
{result.message}
</div>
)}
<div className="flex items-center justify-end pt-2">
<Button type="submit" disabled={loading}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />}
<Bug className="mr-2 h-4 w-4" />
{t("bugreport.submit")}
</Button>
</div>
</form>
</div>
</div>
);
}