website refactor
This commit is contained in:
53
apps/website/components/errors/AppErrorBoundaryView.tsx
Normal file
53
apps/website/components/errors/AppErrorBoundaryView.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
|
||||
interface AppErrorBoundaryViewProps {
|
||||
title: string;
|
||||
description: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* AppErrorBoundaryView
|
||||
*
|
||||
* Semantic container for error boundary content.
|
||||
* Follows "Precision Racing Minimal" theme.
|
||||
*/
|
||||
export function AppErrorBoundaryView({ title, description, children }: AppErrorBoundaryViewProps) {
|
||||
return (
|
||||
<Stack gap={6} align="center" fullWidth>
|
||||
{/* Header Icon */}
|
||||
<Box
|
||||
p={4}
|
||||
rounded="full"
|
||||
bg="bg-warning-amber"
|
||||
bgOpacity={0.1}
|
||||
border
|
||||
borderColor="border-warning-amber"
|
||||
>
|
||||
<Icon icon={AlertTriangle} size={8} color="var(--warning-amber)" />
|
||||
</Box>
|
||||
|
||||
{/* Typography */}
|
||||
<Stack gap={2} align="center">
|
||||
<Heading level={1} weight="bold">
|
||||
<Text uppercase letterSpacing="tighter">
|
||||
{title}
|
||||
</Text>
|
||||
</Heading>
|
||||
<Text color="text-gray-400" align="center" maxWidth="md" leading="relaxed">
|
||||
{description}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
106
apps/website/components/errors/ErrorDetails.tsx
Normal file
106
apps/website/components/errors/ErrorDetails.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { ChevronDown, ChevronUp, Copy, Terminal } from 'lucide-react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
|
||||
interface ErrorDetailsProps {
|
||||
error: Error & { digest?: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* ErrorDetails
|
||||
*
|
||||
* Handles the display of technical error information with a toggle.
|
||||
* Part of the 500 route redesign.
|
||||
*/
|
||||
export function ErrorDetails({ error }: ErrorDetailsProps) {
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copyError = async () => {
|
||||
const details = {
|
||||
message: error.message,
|
||||
digest: error.digest,
|
||||
stack: error.stack,
|
||||
url: typeof window !== 'undefined' ? window.location.href : 'unknown',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(JSON.stringify(details, null, 2));
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
// Silent fail
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={4} fullWidth pt={4} borderTop borderColor="border-white">
|
||||
<Box
|
||||
as="button"
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gap={2}
|
||||
color="text-gray-500"
|
||||
hoverTextColor="text-gray-300"
|
||||
transition
|
||||
>
|
||||
<Icon icon={Terminal} size={3} />
|
||||
<Text
|
||||
size="xs"
|
||||
weight="medium"
|
||||
uppercase
|
||||
letterSpacing="widest"
|
||||
color="inherit"
|
||||
>
|
||||
{showDetails ? 'Hide Technical Logs' : 'Show Technical Logs'}
|
||||
</Text>
|
||||
{showDetails ? <Icon icon={ChevronUp} size={3} /> : <Icon icon={ChevronDown} size={3} />}
|
||||
</Box>
|
||||
|
||||
{showDetails && (
|
||||
<Stack gap={3}>
|
||||
<Surface
|
||||
variant="dark"
|
||||
rounded="md"
|
||||
padding={4}
|
||||
fullWidth
|
||||
maxHeight="48"
|
||||
overflow="auto"
|
||||
border
|
||||
borderColor="border-white"
|
||||
bgOpacity={0.4}
|
||||
hideScrollbar={false}
|
||||
>
|
||||
<Text font="mono" size="xs" color="text-gray-500" block leading="relaxed">
|
||||
{error.stack || 'No stack trace available'}
|
||||
{error.digest && `\n\nDigest: ${error.digest}`}
|
||||
</Text>
|
||||
</Surface>
|
||||
|
||||
<Box display="flex" justifyContent="end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={copyError}
|
||||
icon={<Icon icon={Copy} size={3} />}
|
||||
height="8"
|
||||
fontSize="10px"
|
||||
>
|
||||
{copied ? 'Copied to Clipboard' : 'Copy Error Details'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
107
apps/website/components/errors/ErrorDetailsBlock.tsx
Normal file
107
apps/website/components/errors/ErrorDetailsBlock.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Copy, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
|
||||
interface ErrorDetailsBlockProps {
|
||||
error: Error & { digest?: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* ErrorDetailsBlock
|
||||
*
|
||||
* Semantic component for technical error details.
|
||||
* Follows "Precision Racing Minimal" theme.
|
||||
*/
|
||||
export function ErrorDetailsBlock({ error }: ErrorDetailsBlockProps) {
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copyError = async () => {
|
||||
const details = {
|
||||
message: error.message,
|
||||
digest: error.digest,
|
||||
stack: error.stack,
|
||||
url: typeof window !== 'undefined' ? window.location.href : 'unknown',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(JSON.stringify(details, null, 2));
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
// Silent fail
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={4} fullWidth pt={4} borderTop borderColor="border-white" bgOpacity={0.1}>
|
||||
<Box
|
||||
as="button"
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gap={2}
|
||||
transition
|
||||
>
|
||||
<Text
|
||||
size="xs"
|
||||
color="text-gray-500"
|
||||
hoverTextColor="text-gray-300"
|
||||
uppercase
|
||||
letterSpacing="widest"
|
||||
weight="medium"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={2}
|
||||
>
|
||||
{showDetails ? <Icon icon={ChevronUp} size={3} /> : <Icon icon={ChevronDown} size={3} />}
|
||||
{showDetails ? 'Hide Technical Logs' : 'Show Technical Logs'}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{showDetails && (
|
||||
<Stack gap={3}>
|
||||
<Surface
|
||||
variant="dark"
|
||||
rounded="md"
|
||||
padding={4}
|
||||
fullWidth
|
||||
maxHeight="48"
|
||||
overflow="auto"
|
||||
border
|
||||
borderColor="border-white"
|
||||
bgOpacity={0.4}
|
||||
hideScrollbar={false}
|
||||
>
|
||||
<Text font="mono" size="xs" color="text-gray-500" block leading="relaxed">
|
||||
{error.stack || 'No stack trace available'}
|
||||
{error.digest && `\n\nDigest: ${error.digest}`}
|
||||
</Text>
|
||||
</Surface>
|
||||
|
||||
<Box display="flex" justifyContent="end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={copyError}
|
||||
icon={<Icon icon={Copy} size={3} />}
|
||||
height="8"
|
||||
fontSize="10px"
|
||||
>
|
||||
{copied ? 'Copied to Clipboard' : 'Copy Error Details'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
48
apps/website/components/errors/ErrorRecoveryActions.tsx
Normal file
48
apps/website/components/errors/ErrorRecoveryActions.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { RefreshCw, Home } from 'lucide-react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
|
||||
interface ErrorRecoveryActionsProps {
|
||||
onRetry: () => void;
|
||||
onHome: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* ErrorRecoveryActions
|
||||
*
|
||||
* Semantic component for error recovery buttons.
|
||||
* Follows "Precision Racing Minimal" theme.
|
||||
*/
|
||||
export function ErrorRecoveryActions({ onRetry, onHome }: ErrorRecoveryActionsProps) {
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
flexWrap="wrap"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gap={3}
|
||||
fullWidth
|
||||
>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onRetry}
|
||||
icon={<Icon icon={RefreshCw} size={4} />}
|
||||
width="160px"
|
||||
>
|
||||
Retry Session
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onHome}
|
||||
icon={<Icon icon={Home} size={4} />}
|
||||
width="160px"
|
||||
>
|
||||
Return to Pits
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
52
apps/website/components/errors/ErrorScreen.test.tsx
Normal file
52
apps/website/components/errors/ErrorScreen.test.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { ErrorScreen } from './ErrorScreen';
|
||||
|
||||
describe('ErrorScreen', () => {
|
||||
const mockError = new Error('Test error message');
|
||||
(mockError as any).digest = 'test-digest';
|
||||
(mockError as any).stack = 'test-stack-trace';
|
||||
|
||||
const mockReset = vi.fn();
|
||||
const mockOnHome = vi.fn();
|
||||
|
||||
it('renders error message and system malfunction title', () => {
|
||||
render(<ErrorScreen error={mockError} reset={mockReset} onHome={mockOnHome} />);
|
||||
|
||||
expect(screen.getByText('System Malfunction')).toBeDefined();
|
||||
expect(screen.getByText('Test error message')).toBeDefined();
|
||||
});
|
||||
|
||||
it('calls reset when Retry Session is clicked', () => {
|
||||
render(<ErrorScreen error={mockError} reset={mockReset} onHome={mockOnHome} />);
|
||||
|
||||
const button = screen.getByText('Retry Session');
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(mockReset).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls onHome when Return to Pits is clicked', () => {
|
||||
render(<ErrorScreen error={mockError} reset={mockReset} onHome={mockOnHome} />);
|
||||
|
||||
const button = screen.getByText('Return to Pits');
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(mockOnHome).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('toggles technical logs visibility', () => {
|
||||
render(<ErrorScreen error={mockError} reset={mockReset} onHome={mockOnHome} />);
|
||||
|
||||
expect(screen.queryByText('test-stack-trace')).toBeNull();
|
||||
|
||||
const toggle = screen.getByText('Show Technical Logs');
|
||||
fireEvent.click(toggle);
|
||||
|
||||
expect(screen.getByText(/test-stack-trace/)).toBeDefined();
|
||||
expect(screen.getByText(/Digest: test-digest/)).toBeDefined();
|
||||
|
||||
fireEvent.click(screen.getByText('Hide Technical Logs'));
|
||||
expect(screen.queryByText(/test-stack-trace/)).toBeNull();
|
||||
});
|
||||
});
|
||||
80
apps/website/components/errors/ErrorScreen.tsx
Normal file
80
apps/website/components/errors/ErrorScreen.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Glow } from '@/ui/Glow';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { AppErrorBoundaryView } from './AppErrorBoundaryView';
|
||||
import { ErrorRecoveryActions } from './ErrorRecoveryActions';
|
||||
import { ErrorDetailsBlock } from './ErrorDetailsBlock';
|
||||
|
||||
interface ErrorScreenProps {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
onHome: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* ErrorScreen
|
||||
*
|
||||
* Semantic component for the root-level error boundary.
|
||||
* Follows "Precision Racing Minimal" theme.
|
||||
*/
|
||||
export function ErrorScreen({ error, reset, onHome }: ErrorScreenProps) {
|
||||
return (
|
||||
<Box
|
||||
as="main"
|
||||
minHeight="screen"
|
||||
fullWidth
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
bg="bg-deep-graphite"
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
px={6}
|
||||
>
|
||||
{/* Background Accents */}
|
||||
<Glow color="primary" size="xl" position="center" opacity={0.05} />
|
||||
|
||||
<Surface
|
||||
variant="glass"
|
||||
border
|
||||
rounded="lg"
|
||||
padding={8}
|
||||
maxWidth="2xl"
|
||||
fullWidth
|
||||
position="relative"
|
||||
zIndex={10}
|
||||
shadow="xl"
|
||||
borderColor="border-white"
|
||||
bgOpacity={0.05}
|
||||
>
|
||||
<AppErrorBoundaryView
|
||||
title="System Malfunction"
|
||||
description="The application encountered an unexpected state. Our telemetry has logged the incident."
|
||||
>
|
||||
{/* Error Message Summary */}
|
||||
<Surface
|
||||
variant="dark"
|
||||
rounded="md"
|
||||
padding={4}
|
||||
fullWidth
|
||||
border
|
||||
borderColor="border-white"
|
||||
bgOpacity={0.2}
|
||||
>
|
||||
<Text font="mono" size="sm" color="text-warning-amber" block>
|
||||
{error.message || 'Unknown execution error'}
|
||||
</Text>
|
||||
</Surface>
|
||||
|
||||
<ErrorRecoveryActions onRetry={reset} onHome={onHome} />
|
||||
|
||||
<ErrorDetailsBlock error={error} />
|
||||
</AppErrorBoundaryView>
|
||||
</Surface>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
185
apps/website/components/errors/GlobalErrorScreen.tsx
Normal file
185
apps/website/components/errors/GlobalErrorScreen.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Glow } from '@/ui/Glow';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { AlertTriangle, RefreshCw, Home, Terminal } from 'lucide-react';
|
||||
import { Button } from '@/ui/Button';
|
||||
|
||||
interface GlobalErrorScreenProps {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
onHome: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* GlobalErrorScreen
|
||||
*
|
||||
* A strong, minimal "system fault" view for the root global error boundary.
|
||||
* Instrument-grade UI following the "Precision Racing Minimal" theme.
|
||||
*/
|
||||
export function GlobalErrorScreen({ error, reset, onHome }: GlobalErrorScreenProps) {
|
||||
return (
|
||||
<Box
|
||||
as="main"
|
||||
minHeight="screen"
|
||||
fullWidth
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
bg="bg-base-black"
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
px={6}
|
||||
>
|
||||
{/* Background Accents - Subtle telemetry vibe */}
|
||||
<Glow color="primary" size="xl" position="center" opacity={0.03} />
|
||||
|
||||
<Surface
|
||||
variant="dark"
|
||||
border
|
||||
rounded="none"
|
||||
padding={0}
|
||||
maxWidth="2xl"
|
||||
fullWidth
|
||||
position="relative"
|
||||
zIndex={10}
|
||||
borderColor="border-white"
|
||||
bgOpacity={0.1}
|
||||
>
|
||||
{/* System Status Header */}
|
||||
<Box
|
||||
borderBottom
|
||||
borderColor="border-white"
|
||||
bgOpacity={0.05}
|
||||
px={6}
|
||||
py={4}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Stack direction="row" gap={3} align="center">
|
||||
<Icon icon={AlertTriangle} size={5} color="var(--warning-amber)" />
|
||||
<Heading level={2} weight="bold">
|
||||
<Text uppercase letterSpacing="widest" size="sm">
|
||||
System Fault Detected
|
||||
</Text>
|
||||
</Heading>
|
||||
</Stack>
|
||||
<Text font="mono" size="xs" color="text-gray-500" uppercase>
|
||||
Status: Critical
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box p={8}>
|
||||
<Stack gap={8}>
|
||||
{/* Fault Description */}
|
||||
<Stack gap={4}>
|
||||
<Text color="text-gray-400" size="base" leading="relaxed">
|
||||
The application kernel encountered an unrecoverable execution error.
|
||||
Telemetry has been captured for diagnostic review.
|
||||
</Text>
|
||||
|
||||
<SystemStatusPanel error={error} />
|
||||
</Stack>
|
||||
|
||||
{/* Recovery Actions */}
|
||||
<RecoveryActions onRetry={reset} onHome={onHome} />
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Footer / Metadata */}
|
||||
<Box
|
||||
borderTop
|
||||
borderColor="border-white"
|
||||
bgOpacity={0.05}
|
||||
px={6}
|
||||
py={3}
|
||||
display="flex"
|
||||
justifyContent="end"
|
||||
>
|
||||
<Text font="mono" size="xs" color="text-gray-600">
|
||||
GP-CORE-ERR-{error.digest?.substring(0, 8).toUpperCase() || 'UNKNOWN'}
|
||||
</Text>
|
||||
</Box>
|
||||
</Surface>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemStatusPanel
|
||||
*
|
||||
* Displays technical fault details in an instrument-grade panel.
|
||||
*/
|
||||
function SystemStatusPanel({ error }: { error: Error & { digest?: string } }) {
|
||||
return (
|
||||
<Surface
|
||||
variant="dark"
|
||||
rounded="none"
|
||||
padding={4}
|
||||
fullWidth
|
||||
border
|
||||
borderColor="border-white"
|
||||
bgOpacity={0.2}
|
||||
>
|
||||
<Stack gap={3}>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Icon icon={Terminal} size={3} color="var(--gray-500)" />
|
||||
<Text font="mono" size="xs" color="text-gray-500" uppercase letterSpacing="wider">
|
||||
Fault Log
|
||||
</Text>
|
||||
</Box>
|
||||
<Text font="mono" size="sm" color="text-warning-amber" block>
|
||||
{error.message || 'Unknown execution fault'}
|
||||
</Text>
|
||||
{error.digest && (
|
||||
<Text font="mono" size="xs" color="text-gray-600" block>
|
||||
Digest: {error.digest}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* RecoveryActions
|
||||
*
|
||||
* Clear, instrument-grade recovery options.
|
||||
*/
|
||||
function RecoveryActions({ onRetry, onHome }: { onRetry: () => void; onHome: () => void }) {
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
flexWrap="wrap"
|
||||
alignItems="center"
|
||||
gap={4}
|
||||
fullWidth
|
||||
>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onRetry}
|
||||
icon={<Icon icon={RefreshCw} size={4} />}
|
||||
rounded="none"
|
||||
px={8}
|
||||
>
|
||||
Reboot Session
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onHome}
|
||||
icon={<Icon icon={Home} size={4} />}
|
||||
rounded="none"
|
||||
px={8}
|
||||
>
|
||||
Return to Pits
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
51
apps/website/components/errors/NotFoundActions.tsx
Normal file
51
apps/website/components/errors/NotFoundActions.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
|
||||
interface NotFoundActionsProps {
|
||||
primaryLabel: string;
|
||||
onPrimaryClick: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* NotFoundActions
|
||||
*
|
||||
* Semantic component for the primary actions on the 404 page.
|
||||
* Follows "Precision Racing Minimal" theme with crisp styling.
|
||||
*/
|
||||
export function NotFoundActions({ primaryLabel, onPrimaryClick }: NotFoundActionsProps) {
|
||||
return (
|
||||
<Stack direction="row" gap={4} align="center" justify="center">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={onPrimaryClick}
|
||||
minWidth="200px"
|
||||
>
|
||||
{primaryLabel}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
onClick={() => window.history.back()}
|
||||
>
|
||||
<Stack direction="row" gap={2} align="center">
|
||||
<Box
|
||||
width={2}
|
||||
height={2}
|
||||
rounded="full"
|
||||
bg="soft-steel"
|
||||
/>
|
||||
<Text size="xs" weight="bold" uppercase letterSpacing="widest" color="text-gray-400">
|
||||
Previous Sector
|
||||
</Text>
|
||||
</Stack>
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
34
apps/website/components/errors/NotFoundCallToAction.tsx
Normal file
34
apps/website/components/errors/NotFoundCallToAction.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Button } from '@/ui/Button';
|
||||
|
||||
interface NotFoundCallToActionProps {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* NotFoundCallToAction
|
||||
*
|
||||
* Semantic component for the primary action on the 404 page.
|
||||
* Follows "Precision Racing Minimal" theme with crisp styling.
|
||||
*/
|
||||
export function NotFoundCallToAction({ label, onClick }: NotFoundCallToActionProps) {
|
||||
return (
|
||||
<Stack gap={4} align="center">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={onClick}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
<Text size="xs" color="text-gray-500" uppercase letterSpacing="widest">
|
||||
Telemetry connection lost
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
51
apps/website/components/errors/NotFoundDiagnostics.tsx
Normal file
51
apps/website/components/errors/NotFoundDiagnostics.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Box } from '@/ui/Box';
|
||||
|
||||
interface NotFoundDiagnosticsProps {
|
||||
errorCode: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* NotFoundDiagnostics
|
||||
*
|
||||
* Semantic component for displaying technical error details.
|
||||
* Styled as a telemetry status indicator.
|
||||
*/
|
||||
export function NotFoundDiagnostics({ errorCode }: NotFoundDiagnosticsProps) {
|
||||
return (
|
||||
<Stack gap={3} align="center">
|
||||
<Box
|
||||
px={3}
|
||||
py={1}
|
||||
border
|
||||
borderColor="primary-accent"
|
||||
bg="primary-accent"
|
||||
bgOpacity={0.1}
|
||||
rounded="sm"
|
||||
>
|
||||
<Text
|
||||
size="xs"
|
||||
weight="bold"
|
||||
color="text-primary-accent"
|
||||
uppercase
|
||||
letterSpacing="widest"
|
||||
>
|
||||
{errorCode}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text
|
||||
size="xs"
|
||||
color="text-gray-500"
|
||||
uppercase
|
||||
letterSpacing="widest"
|
||||
weight="medium"
|
||||
>
|
||||
Telemetry connection lost // Sector data unavailable
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
47
apps/website/components/errors/NotFoundHelpLinks.tsx
Normal file
47
apps/website/components/errors/NotFoundHelpLinks.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
|
||||
interface NotFoundHelpLinksProps {
|
||||
links: Array<{ label: string; href: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* NotFoundHelpLinks
|
||||
*
|
||||
* Semantic component for secondary navigation on the 404 page.
|
||||
* Styled as technical metadata links.
|
||||
*/
|
||||
export function NotFoundHelpLinks({ links }: NotFoundHelpLinksProps) {
|
||||
return (
|
||||
<Stack direction="row" gap={6} align="center" wrap center>
|
||||
{links.map((link, index) => (
|
||||
<React.Fragment key={link.href}>
|
||||
<Box
|
||||
as="a"
|
||||
href={link.href}
|
||||
transition
|
||||
display="inline-block"
|
||||
>
|
||||
<Text
|
||||
color="text-gray-400"
|
||||
hoverTextColor="primary-accent"
|
||||
weight="medium"
|
||||
size="xs"
|
||||
letterSpacing="widest"
|
||||
uppercase
|
||||
>
|
||||
{link.label}
|
||||
</Text>
|
||||
</Box>
|
||||
{index < links.length - 1 && (
|
||||
<Box width="1px" height="12px" bg="border-gray" opacity={0.5} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
141
apps/website/components/errors/NotFoundScreen.tsx
Normal file
141
apps/website/components/errors/NotFoundScreen.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Glow } from '@/ui/Glow';
|
||||
import { NotFoundActions } from './NotFoundActions';
|
||||
import { NotFoundHelpLinks } from './NotFoundHelpLinks';
|
||||
import { NotFoundDiagnostics } from './NotFoundDiagnostics';
|
||||
|
||||
interface NotFoundScreenProps {
|
||||
errorCode: string;
|
||||
title: string;
|
||||
message: string;
|
||||
actionLabel: string;
|
||||
onActionClick: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* NotFoundScreen
|
||||
*
|
||||
* App-specific semantic component for 404 states.
|
||||
* Encapsulates the visual representation of the "Off Track" state.
|
||||
* Redesigned for "Precision Racing Minimal" theme.
|
||||
*/
|
||||
export function NotFoundScreen({
|
||||
errorCode,
|
||||
title,
|
||||
message,
|
||||
actionLabel,
|
||||
onActionClick
|
||||
}: NotFoundScreenProps) {
|
||||
const helpLinks = [
|
||||
{ label: 'Support', href: '/support' },
|
||||
{ label: 'Status', href: '/status' },
|
||||
{ label: 'Documentation', href: '/docs' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="main"
|
||||
minHeight="100vh"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
bg="graphite-black"
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
>
|
||||
{/* Background Glow Accent */}
|
||||
<Glow color="primary" size="xl" opacity={0.1} position="center" />
|
||||
|
||||
<Surface
|
||||
variant="glass"
|
||||
border
|
||||
padding={12}
|
||||
rounded="none"
|
||||
maxWidth="2xl"
|
||||
fullWidth
|
||||
mx={6}
|
||||
position="relative"
|
||||
zIndex={10}
|
||||
>
|
||||
<Stack gap={12} align="center" textAlign="center">
|
||||
{/* Header Section */}
|
||||
<Stack gap={4} align="center">
|
||||
<NotFoundDiagnostics errorCode={errorCode} />
|
||||
|
||||
<Text
|
||||
as="h1"
|
||||
size="4xl"
|
||||
weight="bold"
|
||||
color="text-white"
|
||||
letterSpacing="tighter"
|
||||
uppercase
|
||||
block
|
||||
leading="none"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
{/* Visual Separator */}
|
||||
<Box width="full" height="1px" bg="primary-accent" opacity={0.3} position="relative" display="flex" alignItems="center" justifyContent="center">
|
||||
<Box
|
||||
width={3}
|
||||
height={3}
|
||||
bg="primary-accent"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Message Section */}
|
||||
<Text
|
||||
size="xl"
|
||||
color="text-gray-400"
|
||||
maxWidth="lg"
|
||||
leading="relaxed"
|
||||
block
|
||||
weight="medium"
|
||||
>
|
||||
{message}
|
||||
</Text>
|
||||
|
||||
{/* Actions Section */}
|
||||
<NotFoundActions
|
||||
primaryLabel={actionLabel}
|
||||
onPrimaryClick={onActionClick}
|
||||
/>
|
||||
|
||||
{/* Footer Section */}
|
||||
<Box pt={8} width="full">
|
||||
<Box height="1px" width="full" bg="border-gray" opacity={0.1} mb={8} />
|
||||
<NotFoundHelpLinks links={helpLinks} />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Surface>
|
||||
|
||||
{/* Subtle Edge Details */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
height="2px"
|
||||
bg="primary-accent"
|
||||
opacity={0.1}
|
||||
/>
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom={0}
|
||||
left={0}
|
||||
right={0}
|
||||
height="2px"
|
||||
bg="primary-accent"
|
||||
opacity={0.1}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
59
apps/website/components/errors/RecoveryActions.tsx
Normal file
59
apps/website/components/errors/RecoveryActions.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { RefreshCw, Home, LifeBuoy } from 'lucide-react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
|
||||
interface RecoveryActionsProps {
|
||||
onRetry: () => void;
|
||||
onHome: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* RecoveryActions
|
||||
*
|
||||
* Provides primary and secondary recovery paths for the user.
|
||||
* Part of the 500 route redesign.
|
||||
*/
|
||||
export function RecoveryActions({ onRetry, onHome }: RecoveryActionsProps) {
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
flexWrap="wrap"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gap={3}
|
||||
fullWidth
|
||||
>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onRetry}
|
||||
icon={<Icon icon={RefreshCw} size={4} />}
|
||||
width="160px"
|
||||
>
|
||||
Retry Session
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onHome}
|
||||
icon={<Icon icon={Home} size={4} />}
|
||||
width="160px"
|
||||
>
|
||||
Return to Pits
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
as="a"
|
||||
href="https://support.gridpilot.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
icon={<Icon icon={LifeBuoy} size={4} />}
|
||||
width="160px"
|
||||
>
|
||||
Contact Support
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
77
apps/website/components/errors/ServerErrorPanel.tsx
Normal file
77
apps/website/components/errors/ServerErrorPanel.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
|
||||
interface ServerErrorPanelProps {
|
||||
message?: string;
|
||||
incidentId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServerErrorPanel
|
||||
*
|
||||
* Displays the primary error information in an "instrument-grade" style.
|
||||
* Part of the 500 route redesign.
|
||||
*/
|
||||
export function ServerErrorPanel({ message, incidentId }: ServerErrorPanelProps) {
|
||||
return (
|
||||
<Stack gap={6} align="center" fullWidth>
|
||||
{/* Status Indicator */}
|
||||
<Box
|
||||
p={4}
|
||||
rounded="full"
|
||||
bg="bg-warning-amber"
|
||||
bgOpacity={0.1}
|
||||
border
|
||||
borderColor="border-warning-amber"
|
||||
>
|
||||
<Icon icon={AlertTriangle} size={8} color="var(--warning-amber)" />
|
||||
</Box>
|
||||
|
||||
{/* Primary Message */}
|
||||
<Stack gap={2} align="center">
|
||||
<Heading level={1} weight="bold">
|
||||
CRITICAL_SYSTEM_FAILURE
|
||||
</Heading>
|
||||
<Text color="text-gray-400" align="center" maxWidth="md">
|
||||
The application engine encountered an unrecoverable state.
|
||||
Telemetry has been dispatched to engineering.
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
{/* Technical Summary */}
|
||||
<Surface
|
||||
variant="dark"
|
||||
rounded="md"
|
||||
padding={4}
|
||||
fullWidth
|
||||
border
|
||||
borderColor="border-white"
|
||||
bgOpacity={0.2}
|
||||
>
|
||||
<Stack gap={2}>
|
||||
<Text font="mono" size="sm" color="text-warning-amber" block>
|
||||
STATUS: 500_INTERNAL_SERVER_ERROR
|
||||
</Text>
|
||||
{message && (
|
||||
<Text font="mono" size="xs" color="text-gray-400" block>
|
||||
EXCEPTION: {message}
|
||||
</Text>
|
||||
)}
|
||||
{incidentId && (
|
||||
<Text font="mono" size="xs" color="text-gray-500" block>
|
||||
INCIDENT_ID: {incidentId}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Surface>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user