| 'use client'; |
|
|
| import { useEffect, useRef, useSyncExternalStore } from 'react'; |
|
|
| |
| const subscribeToNothing = () => () => {}; |
| const readDebugFlag = () => |
| new URLSearchParams(window.location.search).get('debug') === '1'; |
| |
| |
| const debugFlagOnServer = () => false; |
|
|
| interface ErrorFallbackProps { |
| error: Error & { digest?: string }; |
| reset: () => void; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export default function ErrorFallback({ error, reset }: ErrorFallbackProps) { |
| const showDetail = useSyncExternalStore( |
| subscribeToNothing, |
| readDebugFlag, |
| debugFlagOnServer, |
| ); |
| |
| |
| const reported = useRef(false); |
|
|
| useEffect(() => { |
| if (reported.current) return; |
| reported.current = true; |
|
|
| |
| |
| |
| try { |
| void fetch('/api/client-error', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| message: error.message, |
| stack: error.stack, |
| digest: error.digest, |
| pathname: window.location.pathname + window.location.search, |
| userAgent: navigator.userAgent, |
| }), |
| keepalive: true, |
| }).catch(() => {}); |
| } catch { |
| |
| } |
| }, [error]); |
|
|
| return ( |
| <div className="min-h-screen bg-gray-50 p-4 sm:p-6 lg:p-8"> |
| <main className="max-w-2xl mx-auto"> |
| <div className="bg-white rounded-lg shadow-md p-6 sm:p-8 mt-8"> |
| <h1 className="text-2xl font-semibold text-gray-900">Something went wrong</h1> |
| <p className="mt-3 text-gray-600"> |
| This page hit an unexpected error. Trying again usually works — the rankings and |
| forecast data themselves are unaffected. |
| </p> |
| |
| <div className="mt-6 flex flex-wrap gap-3"> |
| <button |
| onClick={reset} |
| className="px-4 py-2 rounded-md bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400" |
| > |
| Try again |
| </button> |
| {/* Deliberately a plain anchor, not next/link. The rule exists to avoid full |
| page reloads, but a full reload is the point here: client-side navigation |
| would route within the same broken runtime the boundary just caught, and |
| `global-error` renders when the root layout itself failed. */} |
| {/* eslint-disable-next-line @next/next/no-html-link-for-pages */} |
| <a |
| href="/" |
| className="px-4 py-2 rounded-md border border-gray-300 text-gray-700 text-sm font-medium hover:bg-gray-50" |
| > |
| Back to homepage |
| </a> |
| </div> |
| |
| {error.digest && ( |
| <p className="mt-6 text-xs text-gray-500"> |
| Reference: <code className="font-mono text-gray-700">{error.digest}</code> |
| <span className="block mt-1"> |
| Quote this if you report the problem to us. |
| </span> |
| </p> |
| )} |
| |
| {showDetail && ( |
| <div className="mt-6 border-t border-gray-200 pt-4"> |
| <p className="text-xs font-semibold uppercase tracking-wide text-gray-500"> |
| Debug detail |
| </p> |
| <p className="mt-2 text-sm font-medium text-gray-900 break-words"> |
| {error.message || '(no message)'} |
| </p> |
| {error.stack && ( |
| <pre className="mt-3 max-h-80 overflow-auto rounded bg-gray-900 p-3 text-xs leading-relaxed text-gray-100 whitespace-pre-wrap break-words"> |
| {error.stack} |
| </pre> |
| )} |
| </div> |
| )} |
| </div> |
| </main> |
| </div> |
| ); |
| } |
|
|