| 1 | import { Check, Copy } from "lucide-react"; |
| 2 | import { useEffect, useState } from "react"; |
| 3 | import { useLocation, useMatches } from "react-router"; |
| 4 | |
| 5 | import appCss from "../app.css?url"; |
| 6 | import { clientNavigated } from "../lib/stale-build"; |
| 7 | import { debugReport, describeError } from "../lib/developer-mode"; |
| 8 | import { Button } from "./ui/button"; |
| 9 | import { 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 | */ |
| 16 | export 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 | } |