Skip to content
65 linesCodeBlameRaw
1import { Check, Copy } from "lucide-react";
2import { useEffect, useState } from "react";
3import { useLocation, useMatches } from "react-router";
4
5import appCss from "../app.css?url";
6import { clientNavigated } from "../lib/stale-build";
7import { debugReport, describeError } from "../lib/developer-mode";
8import { Button } from "./ui/button";
9import { Card } from "./ui/card";
10
11/**
12 * On a page that failed, with developer mode on (lib/developer-mode.ts):
13 * the error itself, its stack and where it happened, and a button that
14 * copies all of it as one report.
15 */
16export function DebugDetails({ error }: { error: unknown }) {
17 const location = useLocation();
18 const matches = useMatches();
19 // The browser's facts once it is in the browser; the server draws without them.
20 const [browser, setBrowser] = useState<{ url: string; userAgent: string; desktop: string | null; at: Date } | null>(null);
21 useEffect(() => {
22 setBrowser({ url: window.location.href, userAgent: navigator.userAgent, desktop: window.g1tDesktop?.version ?? null, at: new Date() });
23 }, [location.key, error]);
24 const report = debugReport({
25 url: browser?.url ?? location.pathname + location.search + location.hash,
26 at: browser?.at ?? null,
27 error,
28 routes: matches.map((match) => match.id),
29 build: appCss.split("/").pop() ?? appCss,
30 userAgent: browser?.userAgent ?? "",
31 desktop: browser?.desktop,
32 navigation: clientNavigated(location.key) ? "client" : "document",
33 });
34 const described = describeError(error);
35 const [copied, setCopied] = useState(false);
36 const copy = async () => {
37 try {
38 await navigator.clipboard.writeText(report);
39 setCopied(true);
40 window.setTimeout(() => setCopied(false), 2000);
41 } catch {
42 // No clipboard (an old browser, or one that refused): the text is there to select.
43 }
44 };
45 return (
46 <Card tone="plain" radius="lg" className="mt-8 text-left">
47 <div className="flex items-center justify-between gap-3 border-b border-line px-4 py-2.5">
48 <div className="min-w-0">
49 <p className="text-sm font-medium">Developer mode</p>
50 <p className="truncate text-xs text-muted">
51 {described.kind}
52 {described.status ? ` ${described.status}` : ""}: {described.message}
53 </p>
54 </div>
55 <Button variant="outline" size="sm" onClick={copy} className="shrink-0">
56 {copied ? <Check size={14} /> : <Copy size={14} />}
57 {copied ? "Copied" : "Copy details"}
58 </Button>
59 </div>
60 <pre className="max-h-96 overflow-auto p-4 text-xs leading-relaxed whitespace-pre-wrap wrap-break-word">
61 <code>{report}</code>
62 </pre>
63 </Card>
64 );
65}