Files
gridpilot.gg/apps/website/ui/EmptyState.tsx
Marc Mintel e04282d77e
Some checks failed
CI / lint-typecheck (pull_request) Failing after 10s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
code quality
2026-01-27 17:36:39 +01:00

155 lines
5.4 KiB
TypeScript

import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading';
import { Link } from '@/ui/Link';
import { EmptyStateProps } from '@/ui/state-types';
import { Text } from '@/ui/Text';
import React from 'react';
// Illustration components (simple SVG representations)
const Illustrations = {
racing: () => (
<svg width="80" height="80" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 70 L80 70 L85 50 L80 30 L20 30 L15 50 Z" fill="currentColor" opacity="0.2"/>
<path d="M30 60 L70 60 L75 50 L70 40 L30 40 L25 50 Z" fill="currentColor" opacity="0.4"/>
<circle cx="35" cy="65" r="3" fill="currentColor"/>
<circle cx="65" cy="65" r="3" fill="currentColor"/>
<path d="M50 30 L50 20 M45 25 L50 20 L55 25" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
),
league: () => (
<svg width="80" height="80" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="35" r="15" fill="currentColor" opacity="0.3"/>
<path d="M35 50 L50 45 L65 50 L65 70 L35 70 Z" fill="currentColor" opacity="0.2"/>
<path d="M40 55 L50 52 L60 55" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
<path d="M40 62 L50 59 L60 62" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
),
team: () => (
<svg width="80" height="80" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="35" cy="35" r="8" fill="currentColor" opacity="0.3"/>
<circle cx="65" cy="35" r="8" fill="currentColor" opacity="0.3"/>
<circle cx="50" cy="55" r="10" fill="currentColor" opacity="0.2"/>
<path d="M35 45 L35 60 M65 45 L65 60 M50 65 L50 80" stroke="currentColor" strokeWidth="3" strokeLinecap="round"/>
</svg>
),
sponsor: () => (
<svg width="80" height="80" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="25" y="25" width="50" height="50" rx="8" fill="currentColor" opacity="0.2"/>
<path d="M35 50 L45 60 L65 40" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M50 35 L50 65 M40 50 L60 50" stroke="currentColor" strokeWidth="2" opacity="0.5"/>
</svg>
),
driver: () => (
<svg width="80" height="80" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="30" r="8" fill="currentColor" opacity="0.3"/>
<path d="M42 38 L58 38 L55 55 L45 55 Z" fill="currentColor" opacity="0.2"/>
<path d="M45 55 L40 70 M55 55 L60 70" stroke="currentColor" strokeWidth="3" strokeLinecap="round"/>
<circle cx="40" cy="72" r="3" fill="currentColor"/>
<circle cx="60" cy="72" r="3" fill="currentColor"/>
</svg>
),
} as const;
export function EmptyState({
icon: Icon,
title,
description,
action,
variant = 'default',
illustration,
ariaLabel = 'Empty state',
children,
}: EmptyStateProps & { children?: React.ReactNode }) {
const IllustrationComponent = illustration ? Illustrations[illustration] : null;
const content = (
<Box display="flex" flexDirection="col" alignItems="center" gap={4} textAlign="center">
<Box>
{IllustrationComponent ? (
<Box color="var(--ui-color-text-low)">
<IllustrationComponent />
</Box>
) : Icon ? (
<Box padding={4} rounded="xl" bg="var(--ui-color-bg-surface-muted)" style={{ border: '1px solid var(--ui-color-border-default)' }}>
<Icon size={32} color="var(--ui-color-text-low)" />
</Box>
) : null}
</Box>
<Heading data-testid="empty-state-title" level={3} weight="semibold">{title}</Heading>
{description && (
<Text data-testid="empty-state-description" variant="low" leading="relaxed">
{description}
</Text>
)}
{action && (
<Box marginTop={2}>
<Button
variant={action.variant || 'primary'}
onClick={action.onClick}
style={{ minWidth: '140px' }}
>
{action.label}
</Button>
</Box>
)}
{children && (
<Box marginTop={4}>
{children}
</Box>
)}
</Box>
);
if (variant === 'full-page') {
return (
<Box
position="fixed"
inset={0}
bg="var(--ui-color-bg-base)"
display="flex"
alignItems="center"
justifyContent="center"
padding={6}
role="status"
aria-label={ariaLabel}
>
<Box maxWidth="32rem" fullWidth>
{content}
<Box marginTop={8} textAlign="center">
<Text size="sm" variant="low">
Need help? Contact us at{' '}
<Link href="mailto:support@gridpilot.com" variant="primary">
support@gridpilot.com
</Link>
</Text>
</Box>
</Box>
</Box>
);
}
return (
<Box
paddingY={variant === 'minimal' ? 8 : 12}
display="flex"
alignItems="center"
justifyContent="center"
role="status"
aria-label={ariaLabel}
>
<Box maxWidth={variant === 'minimal' ? '24rem' : '32rem'} fullWidth>
{content}
</Box>
</Box>
);
}
export function MinimalEmptyState(props: Omit<EmptyStateProps, 'variant'>) {
return <EmptyState {...props} variant="minimal" />;
}