mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
Final surface sweep for the v1.4.27 tap-target work: the chart band overlay, the error-details action row, and the public-page header links. - `health-chart.tsx`: band overlay used to inset `right: 18px` while the underlying ComposedChart margin is `right: 8`, so the colour band drifted left of the plotted line by 10 px on every viewport. Pin the overlay to the chart's actual right edge. - `error-details.tsx`: drop `size="sm"` from the retry, copy, and report-issue buttons so they sit at the default 40 px primitive height — clears the WCAG 2.5.5 floor on touch. - `auth/login/page.tsx`: back-to-passkey toggle wraps in `inline-flex min-h-11 w-full` so the form-mode swap is reachable on touch. - `privacy/page.tsx` and `about/page.tsx`: header HealthLog + Sign-in links wrap in `inline-flex min-h-11` so the public-page chrome clears the floor.
112 lines
3.8 KiB
TypeScript
112 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { Copy, CheckCircle2, RotateCcw, Bug } from "lucide-react";
|
|
import Link from "next/link";
|
|
import { Button } from "@/components/ui/button";
|
|
import { useTranslations } from "@/lib/i18n/context";
|
|
import { useAppSettings } from "@/components/app-settings-provider";
|
|
|
|
export interface ErrorDetailsProps {
|
|
/** The error object, whether it's a runtime Error or a typed API error. */
|
|
error: Error & { digest?: string };
|
|
/** Pass Next.js' `reset()` callback if available. */
|
|
reset?: () => void;
|
|
/** Request ID propagated from the server (x-request-id), if known. */
|
|
requestId?: string | null;
|
|
/** Extra fields to include in the diagnostic payload. */
|
|
context?: Record<string, unknown>;
|
|
/** Override the "Report bug" link target. */
|
|
reportHref?: string;
|
|
}
|
|
|
|
/**
|
|
* Structured error panel with a copy-to-clipboard payload that includes
|
|
* everything we'd need to reproduce or file an issue: message, stack digest,
|
|
* URL, user agent, locale, timestamp.
|
|
*/
|
|
export function ErrorDetails({
|
|
error,
|
|
reset,
|
|
requestId,
|
|
context,
|
|
reportHref = "/bugreport",
|
|
}: ErrorDetailsProps) {
|
|
const { t, locale } = useTranslations();
|
|
const { bugReportEnabled } = useAppSettings();
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
async function handleCopy() {
|
|
// Only capture the pathname — some flows carry OAuth codes / state tokens
|
|
// in the query string (Withings connect, password reset) and we don't
|
|
// want users pasting those into public GitHub issues. Preserve the count
|
|
// for context without exposing values.
|
|
const loc = typeof window !== "undefined" ? window.location : null;
|
|
const urlPath = loc?.pathname ?? null;
|
|
const searchParamCount = loc?.search
|
|
? new URLSearchParams(loc.search).toString().split("&").filter(Boolean)
|
|
.length
|
|
: 0;
|
|
|
|
const payload = {
|
|
message: error.message,
|
|
digest: error.digest,
|
|
name: error.name,
|
|
stack: error.stack?.split("\n").slice(0, 10).join("\n"),
|
|
requestId: requestId ?? null,
|
|
urlPath,
|
|
searchParamCount,
|
|
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : null,
|
|
locale,
|
|
timestamp: new Date().toISOString(),
|
|
context,
|
|
};
|
|
|
|
const text = JSON.stringify(payload, null, 2);
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
} catch {
|
|
// Fallback: open a prompt the user can copy from manually.
|
|
window.prompt(t("common.copyDetails"), text);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="mx-auto w-full max-w-xl space-y-4 p-6">
|
|
<h1 className="text-xl font-semibold">{t("errorBoundary.title")}</h1>
|
|
<p className="text-muted-foreground text-sm">
|
|
{t("errorBoundary.description")}
|
|
</p>
|
|
<div className="bg-muted/50 text-muted-foreground rounded-md border p-3 font-mono text-xs break-all">
|
|
{error.message || t("common.unknownError")}
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
{reset && (
|
|
<Button onClick={reset} variant="outline">
|
|
<RotateCcw className="mr-2 h-4 w-4" />
|
|
{t("common.retry")}
|
|
</Button>
|
|
)}
|
|
<Button onClick={handleCopy} variant="outline">
|
|
{copied ? (
|
|
<CheckCircle2 className="text-dracula-green mr-2 h-4 w-4" />
|
|
) : (
|
|
<Copy className="mr-2 h-4 w-4" />
|
|
)}
|
|
{copied ? t("common.copied") : t("common.copyDetails")}
|
|
</Button>
|
|
{bugReportEnabled && (
|
|
<Button asChild variant="outline">
|
|
<Link href={reportHref}>
|
|
<Bug className="mr-2 h-4 w-4" />
|
|
{t("common.reportIssue")}
|
|
</Link>
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|