website refactor
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { notFound, redirect } from 'next/navigation';
|
||||
import { DashboardPageQuery } from '@/lib/page-queries/DashboardPageQuery';
|
||||
import { DashboardTemplate } from '@/templates/DashboardTemplate';
|
||||
import { DashboardPageClient } from '@/client-wrapper/DashboardPageClient';
|
||||
import { logger } from '@/lib/infrastructure/logging/logger';
|
||||
|
||||
export default async function DashboardPage() {
|
||||
@@ -23,5 +23,5 @@ export default async function DashboardPage() {
|
||||
|
||||
// Success
|
||||
const viewData = result.unwrap();
|
||||
return <DashboardTemplate viewData={viewData} />;
|
||||
}
|
||||
return <DashboardPageClient viewData={viewData} />;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { CreateLeagueWizard } from '@/client-wrapper/CreateLeagueWizard';
|
||||
import { Section } from '@/ui/Section';
|
||||
import { Container } from '@/ui/Container';
|
||||
@@ -10,11 +6,13 @@ import { SearchParamBuilder } from '@/lib/routing/search-params/SearchParamBuild
|
||||
|
||||
type StepName = 'basics' | 'visibility' | 'structure' | 'schedule' | 'scoring' | 'stewarding' | 'review';
|
||||
|
||||
export default function CreateLeaguePage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
interface CreateLeaguePageProps {
|
||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||
}
|
||||
|
||||
const wizardParams = SearchParamParser.parseWizard(searchParams as unknown as URLSearchParams).unwrap();
|
||||
export default async function CreateLeaguePage({ searchParams }: CreateLeaguePageProps) {
|
||||
const resolvedParams = await searchParams;
|
||||
const wizardParams = SearchParamParser.parseWizard(resolvedParams as any).unwrap();
|
||||
const rawStep = wizardParams.step;
|
||||
|
||||
let currentStepName: StepName = 'basics';
|
||||
@@ -28,23 +26,11 @@ export default function CreateLeaguePage() {
|
||||
currentStepName = rawStep;
|
||||
}
|
||||
|
||||
const handleStepChange = (stepName: StepName) => {
|
||||
const builder = new SearchParamBuilder();
|
||||
// Copy existing params if needed, but here we just want to set the step
|
||||
if (searchParams) {
|
||||
searchParams.forEach((value, key) => {
|
||||
if (key !== 'step') builder.set(key, value);
|
||||
});
|
||||
}
|
||||
builder.step(stepName);
|
||||
router.push(`/leagues/create${builder.build()}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<Container size="md">
|
||||
<CreateLeagueWizard stepName={currentStepName} onStepChange={handleStepChange} />
|
||||
<CreateLeagueWizard stepName={currentStepName} />
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PageWrapper } from '@/components/shared/state/PageWrapper';
|
||||
import { HomeTemplate, type HomeViewData } from '@/templates/HomeTemplate';
|
||||
import { HomePageClient } from '@/client-wrapper/HomePageClient';
|
||||
import { PageDataFetcher } from '@/lib/page/PageDataFetcher';
|
||||
import { HomePageQuery } from '@/lib/page-queries/HomePageQuery';
|
||||
import { notFound, redirect } from 'next/navigation';
|
||||
@@ -19,7 +18,5 @@ export default async function Page() {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const Template = ({ viewData }: { viewData: HomeViewData }) => <HomeTemplate viewData={viewData} />;
|
||||
|
||||
return <PageWrapper data={data} Template={Template} />;
|
||||
return <HomePageClient viewData={data} />;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState } from 'react';
|
||||
import { SponsorSettingsTemplate } from '@/templates/SponsorSettingsTemplate';
|
||||
import { logoutAction } from '@/app/actions/logoutAction';
|
||||
import { ConfirmDialog } from '@/ui/ConfirmDialog';
|
||||
import { ConfirmDialog } from '@/components/shared/ConfirmDialog';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { logger } from '@/lib/infrastructure/logging/logger';
|
||||
|
||||
@@ -1,37 +1,5 @@
|
||||
'use client';
|
||||
import { CreateTeamPageClient } from '@/client-wrapper/CreateTeamPageClient';
|
||||
|
||||
import { CreateTeamForm } from '@/components/teams/CreateTeamForm';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Section } from '@/ui/Section';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function CreateTeamPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const handleNavigate = (teamId: string) => {
|
||||
router.push(routes.team.detail(teamId));
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<Container size="sm">
|
||||
<Stack gap={8}>
|
||||
<Stack gap={2}>
|
||||
<Heading level={1}>Create a Team</Heading>
|
||||
</Stack>
|
||||
<CreateTeamForm
|
||||
onNavigate={handleNavigate}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
export default async function CreateTeamPage() {
|
||||
return <CreateTeamPageClient />;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { Weekday } from '@/lib/types/Weekday';
|
||||
import type { WizardErrors } from '@/lib/types/WizardErrors';
|
||||
import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';
|
||||
import { CreateLeagueWizardTemplate, Step } from '@/templates/CreateLeagueWizardTemplate';
|
||||
import { SearchParamBuilder } from '@/lib/routing/search-params/SearchParamBuilder';
|
||||
import {
|
||||
Award,
|
||||
Calendar,
|
||||
@@ -91,7 +92,7 @@ type LeagueWizardFormModel = LeagueConfigFormModel & {
|
||||
|
||||
interface CreateLeagueWizardProps {
|
||||
stepName: StepName;
|
||||
onStepChange: (stepName: StepName) => void;
|
||||
onStepChange?: (stepName: StepName) => void;
|
||||
}
|
||||
|
||||
function stepNameToStep(stepName: StepName): Step {
|
||||
@@ -200,6 +201,16 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
|
||||
const router = useRouter();
|
||||
const { session } = useAuth();
|
||||
|
||||
const handleStepChange = useCallback((newStepName: StepName) => {
|
||||
if (onStepChange) {
|
||||
onStepChange(newStepName);
|
||||
} else {
|
||||
const builder = new SearchParamBuilder();
|
||||
builder.step(newStepName);
|
||||
router.push(`/leagues/create${builder.build()}`);
|
||||
}
|
||||
}, [onStepChange, router]);
|
||||
|
||||
const step = stepNameToStep(stepName);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [presetsLoading, setPresetsLoading] = useState(true);
|
||||
@@ -330,19 +341,19 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
|
||||
const nextStep = (step < 7 ? ((step + 1) as Step) : step);
|
||||
saveHighestStep(nextStep);
|
||||
setHighestCompletedStep((prev) => Math.max(prev, nextStep));
|
||||
onStepChange(stepToStepName(nextStep));
|
||||
handleStepChange(stepToStepName(nextStep));
|
||||
};
|
||||
|
||||
const goToPreviousStep = () => {
|
||||
const prevStep = (step > 1 ? ((step - 1) as Step) : step);
|
||||
onStepChange(stepToStepName(prevStep));
|
||||
handleStepChange(stepToStepName(prevStep));
|
||||
};
|
||||
|
||||
const goToStep = useCallback((targetStep: Step) => {
|
||||
if (targetStep <= highestCompletedStep) {
|
||||
onStepChange(stepToStepName(targetStep));
|
||||
handleStepChange(stepToStepName(targetStep));
|
||||
}
|
||||
}, [highestCompletedStep, onStepChange]);
|
||||
}, [highestCompletedStep, handleStepChange]);
|
||||
|
||||
const handleSubmit = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -413,7 +424,7 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
|
||||
}));
|
||||
|
||||
if (LeagueWizardCommandModel.hasWizardErrors(allErrors)) {
|
||||
onStepChange('basics');
|
||||
handleStepChange('basics');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
42
apps/website/client-wrapper/CreateTeamPageClient.tsx
Normal file
42
apps/website/client-wrapper/CreateTeamPageClient.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import { CreateTeamForm } from '@/components/teams/CreateTeamForm';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Section } from '@/ui/Section';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
/**
|
||||
* CreateTeamPageClient
|
||||
*
|
||||
* Client wrapper for the Create Team page.
|
||||
*/
|
||||
export function CreateTeamPageClient() {
|
||||
const router = useRouter();
|
||||
|
||||
const handleNavigate = (teamId: string) => {
|
||||
router.push(routes.team.detail(teamId));
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<Container size="sm">
|
||||
<Stack gap={8}>
|
||||
<Stack gap={2}>
|
||||
<Heading level={1}>Create a Team</Heading>
|
||||
</Stack>
|
||||
<CreateTeamForm
|
||||
onNavigate={handleNavigate}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
26
apps/website/client-wrapper/DashboardPageClient.tsx
Normal file
26
apps/website/client-wrapper/DashboardPageClient.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import { DashboardTemplate } from '@/templates/DashboardTemplate';
|
||||
import type { DashboardViewData } from '@/lib/view-data/DashboardViewData';
|
||||
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
|
||||
/**
|
||||
* DashboardPageClient
|
||||
*
|
||||
* Client wrapper for the Dashboard page.
|
||||
* Manages client-side interactions for the dashboard.
|
||||
*/
|
||||
export function DashboardPageClient({ viewData }: ClientWrapperProps<DashboardViewData>) {
|
||||
const router = useRouter();
|
||||
|
||||
const handleNavigateToRaces = () => router.push(routes.public.races);
|
||||
|
||||
return (
|
||||
<DashboardTemplate
|
||||
viewData={viewData}
|
||||
onNavigateToRaces={handleNavigateToRaces}
|
||||
/>
|
||||
);
|
||||
}
|
||||
20
apps/website/client-wrapper/HomePageClient.tsx
Normal file
20
apps/website/client-wrapper/HomePageClient.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { HomeTemplate, type HomeViewData } from '@/templates/HomeTemplate';
|
||||
import { PageWrapper } from '@/components/shared/state/PageWrapper';
|
||||
|
||||
interface HomePageClientProps {
|
||||
viewData: HomeViewData;
|
||||
}
|
||||
|
||||
/**
|
||||
* HomePageClient - Client wrapper for the Home page.
|
||||
* Manages state and handlers for the home page.
|
||||
*/
|
||||
export function HomePageClient({ viewData }: HomePageClientProps) {
|
||||
const Template = ({ viewData }: { viewData: HomeViewData }) => (
|
||||
<HomeTemplate viewData={viewData} />
|
||||
);
|
||||
|
||||
return <PageWrapper data={viewData} Template={Template} />;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { NotificationProvider } from '@/components/notifications/NotificationPro
|
||||
import { NotificationIntegration } from '@/components/errors/NotificationIntegration';
|
||||
import { EnhancedErrorBoundary } from '@/components/errors/EnhancedErrorBoundary';
|
||||
import { DevToolbar } from '@/components/dev/DevToolbar';
|
||||
import { ThemeProvider } from '@/ui/theme/ThemeProvider';
|
||||
import { ThemeProvider } from '@/components/shared/ThemeProvider';
|
||||
import React from 'react';
|
||||
|
||||
interface AppWrapperProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { ProgressLine } from '@/ui/ProgressLine';
|
||||
import { ProgressLine } from '@/components/shared/ProgressLine';
|
||||
import { SectionHeader } from '@/ui/SectionHeader';
|
||||
import React from 'react';
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Footer } from '@/ui/Footer';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { BrandMark } from '@/ui/BrandMark';
|
||||
import { Box } from '@/ui/Box';
|
||||
|
||||
interface AppFooterProps {
|
||||
children?: React.ReactNode;
|
||||
@@ -7,9 +14,76 @@ interface AppFooterProps {
|
||||
|
||||
/**
|
||||
* AppFooter is the bottom section of the application.
|
||||
* It follows the "Telemetry Workspace" structure: matte surface, thin separators, minimal.
|
||||
* Aligned with ControlBar (Header) design.
|
||||
*/
|
||||
export function AppFooter({ children }: AppFooterProps) {
|
||||
return (
|
||||
<Footer />
|
||||
<Surface
|
||||
as="footer"
|
||||
variant="muted"
|
||||
paddingY={8}
|
||||
marginTop="auto"
|
||||
style={{ borderTop: '1px solid var(--ui-color-border-default)' }}
|
||||
>
|
||||
<Container size="xl">
|
||||
{children ? (
|
||||
children
|
||||
) : (
|
||||
<Grid cols={{ base: 1, md: 4 }} gap={12}>
|
||||
<Stack gap={4}>
|
||||
<Stack direction="row" align="center" gap={4}>
|
||||
<BrandMark />
|
||||
<Box display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={2} borderLeft borderColor="[#23272B]" pl={4}>
|
||||
<Box w="4px" h="4px" rounded="full" bg="primary-accent" animate="pulse" />
|
||||
<Text size="xs" variant="low" weight="bold" font="mono" letterSpacing="0.1em">
|
||||
INFRASTRUCTURE
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Text size="sm" variant="low" maxWidth="xs">
|
||||
The ultimate companion for sim racers. Track your performance, manage your team, and compete in leagues.
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack gap={4}>
|
||||
<Text size="xs" weight="bold" variant="high" uppercase letterSpacing="wider">Platform</Text>
|
||||
<Stack gap={2}>
|
||||
<Link href="/leagues" variant="secondary">Leagues</Link>
|
||||
<Link href="/teams" variant="secondary">Teams</Link>
|
||||
<Link href="/leaderboards" variant="secondary">Leaderboards</Link>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Stack gap={4}>
|
||||
<Text size="xs" weight="bold" variant="high" uppercase letterSpacing="wider">Support</Text>
|
||||
<Stack gap={2}>
|
||||
<Link href="/docs" variant="secondary">Documentation</Link>
|
||||
<Link href="/status" variant="secondary">System Status</Link>
|
||||
<Link href="/contact" variant="secondary">Contact Us</Link>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Stack gap={4}>
|
||||
<Text size="xs" weight="bold" variant="high" uppercase letterSpacing="wider">Legal</Text>
|
||||
<Stack gap={2}>
|
||||
<Link href="/privacy" variant="secondary">Privacy Policy</Link>
|
||||
<Link href="/terms" variant="secondary">Terms of Service</Link>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Box borderTop marginTop={8} paddingTop={6} display="flex" justifyContent="between" alignItems="center">
|
||||
<Text size="xs" variant="low">
|
||||
© {new Date().getFullYear()} GridPilot. All rights reserved.
|
||||
</Text>
|
||||
<Stack direction="row" gap={4}>
|
||||
<Text size="xs" variant="low" font="mono">v1.0.0-stable</Text>
|
||||
<Box w="4px" h="4px" rounded="full" bg="success-green" />
|
||||
</Stack>
|
||||
</Box>
|
||||
</Container>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { SectionHeader } from '@/ui/SectionHeader';
|
||||
import React from 'react';
|
||||
|
||||
@@ -24,9 +23,7 @@ export function AuthCard({ children, title, description }: AuthCardProps) {
|
||||
description={description}
|
||||
variant="minimal"
|
||||
/>
|
||||
<Box>
|
||||
{children}
|
||||
</Box>
|
||||
{children}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Group } from '@/ui/Group';
|
||||
import React from 'react';
|
||||
|
||||
interface AuthFooterLinksProps {
|
||||
@@ -14,10 +14,8 @@ interface AuthFooterLinksProps {
|
||||
*/
|
||||
export function AuthFooterLinks({ children }: AuthFooterLinksProps) {
|
||||
return (
|
||||
<Stack as="footer" mt={8} pt={6} borderTop borderStyle="solid" borderColor="outline-steel">
|
||||
<Stack gap={3} align="center">
|
||||
{children}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Group direction="column" gap={3} align="center" fullWidth>
|
||||
{children}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Form } from '@/ui/Form';
|
||||
import { Group } from '@/ui/Group';
|
||||
import React from 'react';
|
||||
|
||||
interface AuthFormProps {
|
||||
@@ -15,8 +16,10 @@ interface AuthFormProps {
|
||||
*/
|
||||
export function AuthForm({ children, onSubmit }: AuthFormProps) {
|
||||
return (
|
||||
<Stack as="form" {...({ onSubmit, noValidate: true } as any)} gap={6}>
|
||||
{children}
|
||||
</Stack>
|
||||
<Form onSubmit={onSubmit}>
|
||||
<Group direction="column" gap={6}>
|
||||
{children}
|
||||
</Group>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
Trophy,
|
||||
Car,
|
||||
} from 'lucide-react';
|
||||
import { WorkflowMockup, WorkflowStep } from '@/ui/WorkflowMockup';
|
||||
import { WorkflowMockup, WorkflowStep } from '@/components/mockups/WorkflowMockup';
|
||||
|
||||
const WORKFLOW_STEPS: WorkflowStep[] = [
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Activity, AlertTriangle, ChevronDown, ChevronUp, MessageSquare, Wrench,
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
// Import our new components
|
||||
import { Accordion } from '@/ui/Accordion';
|
||||
import { Accordion } from '@/components/shared/Accordion';
|
||||
import { Badge } from '@/ui/Badge';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
|
||||
@@ -30,17 +30,17 @@ export function DriverEntryRow({
|
||||
return (
|
||||
<Stack
|
||||
onClick={onClick}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
direction="row"
|
||||
align="center"
|
||||
gap={3}
|
||||
p={3}
|
||||
rounded="xl"
|
||||
cursor="pointer"
|
||||
transition
|
||||
bg={isCurrentUser ? 'bg-primary-blue/10' : 'transparent'}
|
||||
bg={isCurrentUser ? 'rgba(25, 140, 255, 0.1)' : 'transparent'}
|
||||
border
|
||||
borderColor={isCurrentUser ? 'border-primary-blue/30' : 'transparent'}
|
||||
hoverBorderColor={isCurrentUser ? 'primary-blue/40' : 'charcoal-outline/20'}
|
||||
borderColor={isCurrentUser ? 'rgba(25, 140, 255, 0.3)' : 'transparent'}
|
||||
hoverBorderColor={isCurrentUser ? 'rgba(25, 140, 255, 0.4)' : 'var(--ui-color-border-low)'}
|
||||
>
|
||||
<Stack
|
||||
display="flex"
|
||||
@@ -49,9 +49,10 @@ export function DriverEntryRow({
|
||||
w="8"
|
||||
h="8"
|
||||
rounded="lg"
|
||||
bg="bg-iron-gray"
|
||||
color="text-gray-500"
|
||||
style={{ fontWeight: 'bold', fontSize: '0.875rem' }}
|
||||
bg="var(--ui-color-bg-surface-muted)"
|
||||
color="var(--ui-color-text-low)"
|
||||
weight="bold"
|
||||
fontSize="0.875rem"
|
||||
>
|
||||
{index + 1}
|
||||
</Stack>
|
||||
@@ -63,7 +64,7 @@ export function DriverEntryRow({
|
||||
rounded="full"
|
||||
overflow="hidden"
|
||||
border={isCurrentUser}
|
||||
borderColor={isCurrentUser ? 'border-primary-blue' : ''}
|
||||
borderColor={isCurrentUser ? 'var(--ui-color-intent-primary)' : ''}
|
||||
>
|
||||
<Image
|
||||
src={avatarUrl}
|
||||
@@ -81,11 +82,11 @@ export function DriverEntryRow({
|
||||
w="5"
|
||||
h="5"
|
||||
rounded="full"
|
||||
bg="bg-deep-graphite"
|
||||
bg="var(--ui-color-bg-base)"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
style={{ fontSize: '0.625rem' }}
|
||||
fontSize="0.625rem"
|
||||
>
|
||||
{CountryFlagDisplay.fromCountryCode(country).toString()}
|
||||
</Stack>
|
||||
@@ -96,14 +97,14 @@ export function DriverEntryRow({
|
||||
<Text
|
||||
weight="semibold"
|
||||
size="sm"
|
||||
color={isCurrentUser ? 'text-primary-blue' : 'text-white'}
|
||||
variant={isCurrentUser ? 'primary' : 'high'}
|
||||
truncate
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
{isCurrentUser && <Badge variant="primary">You</Badge>}
|
||||
</Stack>
|
||||
<Text size="xs" color="text-gray-500">{country}</Text>
|
||||
<Text size="xs" variant="low">{country}</Text>
|
||||
</Stack>
|
||||
|
||||
{rating != null && (
|
||||
|
||||
@@ -29,10 +29,10 @@ export function DriverHeaderPanel({
|
||||
|
||||
return (
|
||||
<Stack
|
||||
bg="bg-panel-gray"
|
||||
bg="var(--ui-color-bg-surface)"
|
||||
rounded="xl"
|
||||
border
|
||||
borderColor="border-charcoal-outline"
|
||||
borderColor="var(--ui-color-border-low)"
|
||||
overflow="hidden"
|
||||
position="relative"
|
||||
>
|
||||
@@ -43,12 +43,12 @@ export function DriverHeaderPanel({
|
||||
left={0}
|
||||
right={0}
|
||||
height="24"
|
||||
bg="bg-gradient-to-r from-primary-blue/20 to-transparent"
|
||||
bg="linear-gradient(to right, rgba(25, 140, 255, 0.2), transparent)"
|
||||
opacity={0.5}
|
||||
/>
|
||||
|
||||
<Stack p={6} position="relative">
|
||||
<Stack direction={{ base: 'col', md: 'row' }} gap={6} align="start" className="md:items-center">
|
||||
<Stack direction={{ base: 'col', md: 'row' }} gap={6} align={{ base: 'start', md: 'center' }}>
|
||||
{/* Avatar */}
|
||||
<Stack
|
||||
width="32"
|
||||
@@ -56,8 +56,8 @@ export function DriverHeaderPanel({
|
||||
rounded="2xl"
|
||||
overflow="hidden"
|
||||
border
|
||||
borderColor="border-charcoal-outline"
|
||||
bg="bg-graphite-black"
|
||||
borderColor="var(--ui-color-border-low)"
|
||||
bg="var(--ui-color-bg-base)"
|
||||
flexShrink={0}
|
||||
>
|
||||
<Image
|
||||
@@ -72,25 +72,25 @@ export function DriverHeaderPanel({
|
||||
<Stack flexGrow={1}>
|
||||
<Stack gap={2}>
|
||||
<Stack direction="row" align="center" gap={3} wrap>
|
||||
<Text as="h1" size="3xl" weight="bold" color="text-white">
|
||||
<Text as="h1" size="3xl" weight="bold" variant="high">
|
||||
{name}
|
||||
</Text>
|
||||
<RatingBadge rating={rating} ratingLabel={ratingLabel} size="lg" />
|
||||
</Stack>
|
||||
|
||||
<Stack direction="row" align="center" gap={4} wrap>
|
||||
<Text size="sm" color="text-gray-400">
|
||||
<Text size="sm" variant="low">
|
||||
{nationality}
|
||||
</Text>
|
||||
{globalRankLabel && (
|
||||
<Text size="sm" color="text-gray-400">
|
||||
Global Rank: <Text color="text-warning-amber" weight="semibold">{globalRankLabel}</Text>
|
||||
<Text size="sm" variant="low">
|
||||
Global Rank: <Text variant="warning" weight="semibold">{globalRankLabel}</Text>
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{bio && (
|
||||
<Text size="sm" color="text-gray-400" className="max-w-2xl mt-2" lineClamp={2}>
|
||||
<Text size="sm" variant="low" maxWidth="2xl" mt={2} lineClamp={2}>
|
||||
{bio}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Group } from '@/ui/Group';
|
||||
import { Input } from '@/ui/Input';
|
||||
import { Search } from 'lucide-react';
|
||||
|
||||
@@ -11,13 +11,13 @@ interface DriverSearchBarProps {
|
||||
|
||||
export function DriverSearchBar({ query, onChange }: DriverSearchBarProps) {
|
||||
return (
|
||||
<Box position="relative" group>
|
||||
<Group fullWidth>
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder="Search drivers by name or nationality..."
|
||||
icon={<Search size={20} />}
|
||||
/>
|
||||
</Box>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Group } from '@/ui/Group';
|
||||
import { Table, TableHead, TableBody, TableRow, TableHeaderCell } from '@/ui/Table';
|
||||
import { TrendingUp } from 'lucide-react';
|
||||
import { Card } from '@/ui/Card';
|
||||
@@ -15,16 +15,16 @@ interface DriverTableProps {
|
||||
|
||||
export function DriverTable({ children }: DriverTableProps) {
|
||||
return (
|
||||
<Stack gap={4}>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Group direction="column" gap={4} fullWidth>
|
||||
<Group direction="row" align="center" gap={3}>
|
||||
<Card variant="dark">
|
||||
<Icon icon={TrendingUp} size={5} intent="primary" />
|
||||
</Card>
|
||||
<Stack>
|
||||
<Group direction="column">
|
||||
<Heading level={2}>Driver Rankings</Heading>
|
||||
<Text size="xs" variant="low">Top performers by skill rating</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Table>
|
||||
<TableHead>
|
||||
@@ -40,6 +40,6 @@ export function DriverTable({ children }: DriverTableProps) {
|
||||
{children}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { RatingBadge } from '@/components/drivers/RatingBadge';
|
||||
import { Image } from '@/ui/Image';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Group } from '@/ui/Group';
|
||||
import { TableRow, TableCell } from '@/ui/Table';
|
||||
import { Avatar } from '@/ui/Avatar';
|
||||
|
||||
@@ -41,7 +40,7 @@ export function DriverTableRow({
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Group direction="row" align="center" gap={3}>
|
||||
<Avatar
|
||||
src={avatarUrl || undefined}
|
||||
alt={name}
|
||||
@@ -54,7 +53,7 @@ export function DriverTableRow({
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text size="xs" variant="low">{nationality}</Text>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Group } from '@/ui/Group';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Section } from '@/ui/Section';
|
||||
import { Container } from '@/ui/Container';
|
||||
@@ -41,31 +41,31 @@ export function DriversDirectoryHeader({
|
||||
return (
|
||||
<Section variant="dark" padding="md">
|
||||
<Container>
|
||||
<Stack direction="row" align="center" justify="between" gap={8}>
|
||||
<Stack gap={6}>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Group direction="row" align="center" justify="between" gap={8} fullWidth>
|
||||
<Group direction="column" gap={6}>
|
||||
<Group direction="row" align="center" gap={3}>
|
||||
<Card variant="dark">
|
||||
<Icon icon={Users} size={6} intent="primary" />
|
||||
</Card>
|
||||
<Heading level={1}>Drivers</Heading>
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
<Text size="lg" variant="low">
|
||||
Meet the racers who make every lap count. From rookies to champions, track their journey and see who's dominating the grid.
|
||||
Meet the racers who make every lap count. From rookies to champions, track their journey and see who's dominating the grid.
|
||||
</Text>
|
||||
|
||||
<Stack direction="row" gap={6} wrap>
|
||||
<Group direction="row" gap={6} wrap>
|
||||
{stats.map((stat, index) => (
|
||||
<Stack key={index} direction="row" align="center" gap={2}>
|
||||
<Group key={index} direction="row" align="center" gap={2}>
|
||||
<Text size="sm" variant="low">
|
||||
<Text as="span" weight="semibold" variant="high">{stat.value}</Text> {stat.label}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Stack gap={2} align="center">
|
||||
<Group direction="column" gap={2} align="center">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onViewLeaderboard}
|
||||
@@ -76,8 +76,8 @@ export function DriversDirectoryHeader({
|
||||
<Text size="xs" variant="low" align="center">
|
||||
See full driver rankings
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Group>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
|
||||
@@ -25,10 +25,10 @@ interface LiveryCardProps {
|
||||
|
||||
export function LiveryCard({ livery, onEdit, onDownload, onDelete }: LiveryCardProps) {
|
||||
return (
|
||||
<Card className="overflow-hidden hover:border-primary-blue/50 transition-colors">
|
||||
<Card overflow="hidden" hoverBorderColor="var(--ui-color-intent-primary)" transition="all 0.3s ease">
|
||||
{/* Livery Preview */}
|
||||
<Stack height={48} backgroundColor="deep-graphite" rounded="lg" mb={4} display="flex" center border borderColor="charcoal-outline">
|
||||
<Icon icon={Car} size={16} color="text-gray-600" />
|
||||
<Stack height={48} bg="var(--ui-color-bg-base)" rounded="lg" mb={4} display="flex" center border borderColor="var(--ui-color-border-low)">
|
||||
<Icon icon={Car} size={16} intent="low" />
|
||||
</Stack>
|
||||
|
||||
{/* Livery Info */}
|
||||
|
||||
@@ -27,7 +27,7 @@ export function PerformanceOverview({ stats }: PerformanceOverviewProps) {
|
||||
return (
|
||||
<Card>
|
||||
<Stack mb={6}>
|
||||
<Heading level={2} icon={<Icon icon={Activity} size={5} color="#00f2ff" />}>
|
||||
<Heading level={2} icon={<Icon icon={Activity} size={5} intent="telemetry" />}>
|
||||
Performance Overview
|
||||
</Heading>
|
||||
</Stack>
|
||||
@@ -39,13 +39,13 @@ export function PerformanceOverview({ stats }: PerformanceOverviewProps) {
|
||||
value={stats.wins}
|
||||
max={stats.totalRaces}
|
||||
label="Win Rate"
|
||||
color="#10b981"
|
||||
color="var(--ui-color-intent-success)"
|
||||
/>
|
||||
<CircularProgress
|
||||
value={stats.podiums}
|
||||
max={stats.totalRaces}
|
||||
label="Podium Rate"
|
||||
color="#f59e0b"
|
||||
color="var(--ui-color-intent-warning)"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row" gap={6}>
|
||||
@@ -53,13 +53,13 @@ export function PerformanceOverview({ stats }: PerformanceOverviewProps) {
|
||||
value={stats.consistency ?? 0}
|
||||
max={100}
|
||||
label="Consistency"
|
||||
color="#3b82f6"
|
||||
color="var(--ui-color-intent-primary)"
|
||||
/>
|
||||
<CircularProgress
|
||||
value={stats.totalRaces - stats.dnfs}
|
||||
max={stats.totalRaces}
|
||||
label="Finish Rate"
|
||||
color="#00f2ff"
|
||||
color="var(--ui-color-intent-telemetry)"
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -67,37 +67,37 @@ export function PerformanceOverview({ stats }: PerformanceOverviewProps) {
|
||||
|
||||
<GridItem colSpan={12} lgSpan={6}>
|
||||
<Stack mb={4}>
|
||||
<Heading level={3} icon={<Icon icon={BarChart3} size={4} color="#9ca3af" />}>
|
||||
<Heading level={3} icon={<Icon icon={BarChart3} size={4} intent="low" />}>
|
||||
Results Breakdown
|
||||
</Heading>
|
||||
</Stack>
|
||||
<HorizontalBarChart
|
||||
data={[
|
||||
{ label: 'Wins', value: stats.wins, color: 'bg-performance-green' },
|
||||
{ label: 'Podiums (2nd-3rd)', value: stats.podiums - stats.wins, color: 'bg-warning-amber' },
|
||||
{ label: 'DNFs', value: stats.dnfs, color: 'bg-red-500' },
|
||||
{ label: 'Wins', value: stats.wins, color: 'var(--ui-color-intent-success)' },
|
||||
{ label: 'Podiums (2nd-3rd)', value: stats.podiums - stats.wins, color: 'var(--ui-color-intent-warning)' },
|
||||
{ label: 'DNFs', value: stats.dnfs, color: 'var(--ui-color-intent-critical)' },
|
||||
]}
|
||||
maxValue={stats.totalRaces}
|
||||
/>
|
||||
|
||||
<Stack mt={6}>
|
||||
<Grid cols={2} gap={4}>
|
||||
<Stack p={4} style={{ backgroundColor: '#0f1115', borderRadius: '0.75rem', border: '1px solid #262626' }}>
|
||||
<Stack p={4} bg="var(--ui-color-bg-base)" rounded="xl" border borderColor="var(--ui-color-border-low)">
|
||||
<Stack gap={2}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={TrendingUp} size={4} color="#10b981" />
|
||||
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase' }}>Best Finish</Text>
|
||||
<Icon icon={TrendingUp} size={4} intent="success" />
|
||||
<Text size="xs" variant="low" uppercase>Best Finish</Text>
|
||||
</Stack>
|
||||
<Text size="2xl" weight="bold" color="text-performance-green">P{stats.bestFinish}</Text>
|
||||
<Text size="2xl" weight="bold" variant="success">P{stats.bestFinish}</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack p={4} style={{ backgroundColor: '#0f1115', borderRadius: '0.75rem', border: '1px solid #262626' }}>
|
||||
<Stack p={4} bg="var(--ui-color-bg-base)" rounded="xl" border borderColor="var(--ui-color-border-low)">
|
||||
<Stack gap={2}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={Target} size={4} color="#3b82f6" />
|
||||
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase' }}>Avg Finish</Text>
|
||||
<Icon icon={Target} size={4} intent="primary" />
|
||||
<Text size="xs" variant="low" uppercase>Avg Finish</Text>
|
||||
</Stack>
|
||||
<Text size="2xl" weight="bold" color="text-primary-blue">
|
||||
<Text size="2xl" weight="bold" variant="primary">
|
||||
P{(stats.avgFinish ?? 0).toFixed(1)}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -9,6 +9,8 @@ import { Link } from '@/ui/Link';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Calendar, Clock, ExternalLink, Globe, Star, Trophy, UserPlus } from 'lucide-react';
|
||||
|
||||
interface ProfileHeroProps {
|
||||
@@ -54,25 +56,40 @@ export function ProfileHero({
|
||||
friendRequestSent,
|
||||
}: ProfileHeroProps) {
|
||||
return (
|
||||
<Surface variant="muted" rounded="2xl" border padding={6} style={{ background: 'linear-gradient(to bottom right, rgba(38, 38, 38, 0.8), rgba(38, 38, 38, 0.6), #0f1115)', borderColor: '#262626' }}>
|
||||
<Surface
|
||||
variant="muted"
|
||||
rounded="2xl"
|
||||
border
|
||||
padding={6}
|
||||
bg="linear-gradient(to bottom right, rgba(38, 38, 38, 0.8), rgba(38, 38, 38, 0.6), var(--ui-color-bg-base))"
|
||||
borderColor="var(--ui-color-border-low)"
|
||||
>
|
||||
<Stack direction="row" align="start" gap={6} wrap>
|
||||
{/* Avatar */}
|
||||
<Stack style={{ position: 'relative' }}>
|
||||
<Stack style={{ width: '7rem', height: '7rem', borderRadius: '1rem', background: 'linear-gradient(to bottom right, #3b82f6, #9333ea)', padding: '0.25rem', boxShadow: '0 20px 25px -5px rgba(59, 130, 246, 0.2)' }}>
|
||||
<Stack style={{ width: '100%', height: '100%', borderRadius: '0.75rem', overflow: 'hidden', backgroundColor: '#262626' }}>
|
||||
<Box position="relative">
|
||||
<Stack
|
||||
w="28"
|
||||
h="28"
|
||||
rounded="xl"
|
||||
bg="linear-gradient(to bottom right, var(--ui-color-intent-primary), rgba(147, 51, 234, 1))"
|
||||
padding={1}
|
||||
shadow="xl"
|
||||
>
|
||||
<Stack fullWidth fullHeight rounded="lg" overflow="hidden" bg="var(--ui-color-bg-surface-muted)">
|
||||
<Image
|
||||
src={driver.avatarUrl || mediaConfig.avatars.defaultFallback}
|
||||
alt={driver.name}
|
||||
width={144}
|
||||
height={144}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
objectFit="cover"
|
||||
fill
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Driver Info */}
|
||||
<Stack style={{ flex: 1, minWidth: 0 }}>
|
||||
<Stack flex={1} minWidth="0">
|
||||
<Stack direction="row" align="center" gap={3} wrap mb={2}>
|
||||
<Heading level={1}>{driver.name}</Heading>
|
||||
<Text size="4xl" aria-label={`Country: ${driver.country}`}>
|
||||
@@ -84,18 +101,18 @@ export function ProfileHero({
|
||||
<Stack direction="row" align="center" gap={4} wrap mb={4}>
|
||||
{stats && (
|
||||
<>
|
||||
<Surface variant="muted" rounded="lg" padding={1} style={{ backgroundColor: 'rgba(59, 130, 246, 0.1)', border: '1px solid rgba(59, 130, 246, 0.3)', paddingLeft: '0.75rem', paddingRight: '0.75rem' }}>
|
||||
<Surface variant="muted" rounded="lg" padding={1} bg="rgba(25, 140, 255, 0.1)" border borderColor="rgba(25, 140, 255, 0.3)" px={3}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Star style={{ width: '1rem', height: '1rem', color: '#3b82f6' }} />
|
||||
<Text font="mono" weight="bold" color="text-primary-blue">{stats.ratingLabel}</Text>
|
||||
<Text size="xs" color="text-gray-400">Rating</Text>
|
||||
<Icon icon={Star} size={4} intent="primary" />
|
||||
<Text font="mono" weight="bold" variant="primary">{stats.ratingLabel}</Text>
|
||||
<Text size="xs" variant="low">Rating</Text>
|
||||
</Stack>
|
||||
</Surface>
|
||||
<Surface variant="muted" rounded="lg" padding={1} style={{ backgroundColor: 'rgba(250, 204, 21, 0.1)', border: '1px solid rgba(250, 204, 21, 0.3)', paddingLeft: '0.75rem', paddingRight: '0.75rem' }}>
|
||||
<Surface variant="muted" rounded="lg" padding={1} bg="rgba(250, 204, 21, 0.1)" border borderColor="rgba(250, 204, 21, 0.3)" px={3}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Trophy style={{ width: '1rem', height: '1rem', color: '#facc15' }} />
|
||||
<Text font="mono" weight="bold" style={{ color: '#facc15' }}>{globalRankLabel}</Text>
|
||||
<Text size="xs" color="text-gray-400">Global</Text>
|
||||
<Icon icon={Trophy} size={4} intent="warning" />
|
||||
<Text font="mono" weight="bold" color="rgba(250, 204, 21, 1)">{globalRankLabel}</Text>
|
||||
<Text size="xs" variant="low">Global</Text>
|
||||
</Stack>
|
||||
</Surface>
|
||||
</>
|
||||
@@ -103,19 +120,19 @@ export function ProfileHero({
|
||||
</Stack>
|
||||
|
||||
{/* Meta info */}
|
||||
<Stack direction="row" align="center" gap={4} wrap style={{ fontSize: '0.875rem', color: '#9ca3af' }}>
|
||||
<Stack direction="row" align="center" gap={4} wrap color="var(--ui-color-text-low)">
|
||||
<Stack direction="row" align="center" gap={1.5}>
|
||||
<Globe style={{ width: '1rem', height: '1rem' }} />
|
||||
<Icon icon={Globe} size={4} />
|
||||
<Text size="sm">iRacing: {driver.iracingId}</Text>
|
||||
</Stack>
|
||||
<Stack direction="row" align="center" gap={1.5}>
|
||||
<Calendar style={{ width: '1rem', height: '1rem' }} />
|
||||
<Icon icon={Calendar} size={4} />
|
||||
<Text size="sm">
|
||||
Joined {driver.joinedAtLabel}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack direction="row" align="center" gap={1.5}>
|
||||
<Clock style={{ width: '1rem', height: '1rem' }} />
|
||||
<Icon icon={Clock} size={4} />
|
||||
<Text size="sm">{timezone}</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -127,7 +144,7 @@ export function ProfileHero({
|
||||
variant="primary"
|
||||
onClick={onAddFriend}
|
||||
disabled={friendRequestSent}
|
||||
icon={<UserPlus style={{ width: '1rem', height: '1rem' }} />}
|
||||
icon={<Icon icon={UserPlus} size={4} />}
|
||||
>
|
||||
{friendRequestSent ? 'Request Sent' : 'Add Friend'}
|
||||
</Button>
|
||||
@@ -136,23 +153,22 @@ export function ProfileHero({
|
||||
|
||||
{/* Social Handles */}
|
||||
{socialHandles.length > 0 && (
|
||||
<Stack mt={6} pt={6} style={{ borderTop: '1px solid rgba(38, 38, 38, 0.5)' }}>
|
||||
<Stack mt={6} pt={6} borderTop borderColor="rgba(255, 255, 255, 0.05)">
|
||||
<Stack direction="row" align="center" gap={2} wrap>
|
||||
<Text size="sm" color="text-gray-500" style={{ marginRight: '0.5rem' }}>Connect:</Text>
|
||||
<Text size="sm" variant="low" mr={2}>Connect:</Text>
|
||||
{socialHandles.map((social) => {
|
||||
const Icon = getSocialIcon(social.platform);
|
||||
const SocialIcon = getSocialIcon(social.platform);
|
||||
return (
|
||||
<Stack key={social.platform}>
|
||||
<Link
|
||||
href={social.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="ghost"
|
||||
>
|
||||
<Surface variant="muted" rounded="lg" padding={1} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', paddingLeft: '0.75rem', paddingRight: '0.75rem', backgroundColor: 'rgba(38, 38, 38, 0.5)', border: '1px solid #262626', color: '#9ca3af' }}>
|
||||
<Icon style={{ width: '1rem', height: '1rem' }} />
|
||||
<Surface variant="muted" rounded="lg" padding={1} display="flex" alignItems="center" gap={2} px={3} bg="rgba(255, 255, 255, 0.05)" border borderColor="var(--ui-color-border-low)" color="var(--ui-color-text-low)">
|
||||
<Icon icon={SocialIcon} size={4} />
|
||||
<Text size="sm">{social.handle}</Text>
|
||||
<ExternalLink style={{ width: '0.75rem', height: '0.75rem', opacity: 0.5 }} />
|
||||
<Icon icon={ExternalLink} size={3} opacity={0.5} />
|
||||
</Surface>
|
||||
</Link>
|
||||
</Stack>
|
||||
|
||||
@@ -7,10 +7,10 @@ import { Text } from '@/ui/Text';
|
||||
import { BarChart3 } from 'lucide-react';
|
||||
|
||||
const SKILL_LEVELS = [
|
||||
{ id: 'pro', label: 'Pro', icon: BarChart3, color: 'text-yellow-400', bgColor: 'bg-yellow-400/10', borderColor: 'border-yellow-400/30' },
|
||||
{ id: 'advanced', label: 'Advanced', icon: BarChart3, color: 'text-purple-400', bgColor: 'bg-purple-400/10', borderColor: 'border-purple-400/30' },
|
||||
{ id: 'intermediate', label: 'Intermediate', icon: BarChart3, color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', borderColor: 'border-primary-blue/30' },
|
||||
{ id: 'beginner', label: 'Beginner', icon: BarChart3, color: 'text-green-400', bgColor: 'bg-green-400/10', borderColor: 'border-green-400/30' },
|
||||
{ id: 'pro', label: 'Pro', icon: BarChart3, color: 'var(--ui-color-intent-warning)', bgColor: 'rgba(255, 190, 77, 0.1)', borderColor: 'rgba(255, 190, 77, 0.3)' },
|
||||
{ id: 'advanced', label: 'Advanced', icon: BarChart3, color: 'rgba(147, 51, 234, 1)', bgColor: 'rgba(147, 51, 234, 0.1)', borderColor: 'rgba(147, 51, 234, 0.3)' },
|
||||
{ id: 'intermediate', label: 'Intermediate', icon: BarChart3, color: 'var(--ui-color-intent-primary)', bgColor: 'rgba(25, 140, 255, 0.1)', borderColor: 'rgba(25, 140, 255, 0.3)' },
|
||||
{ id: 'beginner', label: 'Beginner', icon: BarChart3, color: 'var(--ui-color-intent-success)', bgColor: 'rgba(16, 185, 129, 0.1)', borderColor: 'rgba(16, 185, 129, 0.3)' },
|
||||
];
|
||||
|
||||
interface SkillDistributionProps {
|
||||
@@ -38,15 +38,15 @@ export function SkillDistribution({ drivers }: SkillDistributionProps) {
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
rounded="xl"
|
||||
bg="bg-neon-aqua/10"
|
||||
bg="rgba(78, 212, 224, 0.1)"
|
||||
border
|
||||
borderColor="border-neon-aqua/20"
|
||||
borderColor="rgba(78, 212, 224, 0.2)"
|
||||
>
|
||||
<Icon icon={BarChart3} size={5} color="var(--neon-aqua)" />
|
||||
<Icon icon={BarChart3} size={5} intent="telemetry" />
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading level={2}>Skill Distribution</Heading>
|
||||
<Text size="xs" color="text-gray-500">Driver population by skill level</Text>
|
||||
<Text size="xs" variant="low">Driver population by skill level</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -58,28 +58,24 @@ export function SkillDistribution({ drivers }: SkillDistributionProps) {
|
||||
p={4}
|
||||
rounded="xl"
|
||||
border
|
||||
className={`${level.bgColor} ${level.borderColor}`}
|
||||
bg={level.bgColor}
|
||||
borderColor={level.borderColor}
|
||||
>
|
||||
<Box display="flex" alignItems="center" justifyContent="between" mb={3}>
|
||||
<Icon icon={level.icon} size={5} className={level.color} />
|
||||
<Text size="2xl" weight="bold" className={level.color}>{level.count}</Text>
|
||||
<Icon icon={level.icon} size={5} color={level.color} />
|
||||
<Text size="2xl" weight="bold" color={level.color}>{level.count}</Text>
|
||||
</Box>
|
||||
<Text color="text-white" weight="medium" block mb={1}>{level.label}</Text>
|
||||
<Box fullWidth h="2" rounded="full" bg="bg-deep-graphite/50" overflow="hidden">
|
||||
<Text variant="high" weight="medium" block mb={1}>{level.label}</Text>
|
||||
<Box fullWidth h="2" rounded="full" bg="var(--ui-color-bg-base)" overflow="hidden">
|
||||
<Box
|
||||
h="full"
|
||||
rounded="full"
|
||||
transition
|
||||
className={
|
||||
level.id === 'pro' ? 'bg-yellow-400' :
|
||||
level.id === 'advanced' ? 'bg-purple-400' :
|
||||
level.id === 'intermediate' ? 'bg-primary-blue' :
|
||||
'bg-green-400'
|
||||
}
|
||||
bg={level.color}
|
||||
style={{ width: `${level.percentage}%` }}
|
||||
/>
|
||||
</Box>
|
||||
<Text size="xs" color="text-gray-500" block mt={1}>{level.percentage}% of drivers</Text>
|
||||
<Text size="xs" variant="low" block mt={1}>{level.percentage}% of drivers</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import React, { Component, ReactNode, useState } from 'react';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import { connectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
|
||||
import { ErrorDisplay } from '@/ui/ErrorDisplay';
|
||||
import { DevErrorPanel } from '@/ui/DevErrorPanel';
|
||||
import { ErrorDisplay } from '@/components/shared/ErrorDisplay';
|
||||
import { DevErrorPanel } from '@/components/shared/DevErrorPanel';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import React, { Component, ReactNode, ErrorInfo, useState, version } from 'react';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
|
||||
import { DevErrorPanel } from '@/ui/DevErrorPanel';
|
||||
import { ErrorDisplay } from '@/ui/ErrorDisplay';
|
||||
import { DevErrorPanel } from '@/components/shared/DevErrorPanel';
|
||||
import { ErrorDisplay } from '@/components/shared/ErrorDisplay';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Icon } from '@/ui/Icon';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Accordion } from '@/ui/Accordion';
|
||||
import { Accordion } from '@/components/shared/Accordion';
|
||||
import { Copy } from 'lucide-react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Icon } from '@/ui/Icon';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Accordion } from '@/ui/Accordion';
|
||||
import { Accordion } from '@/components/shared/Accordion';
|
||||
import { Copy } from 'lucide-react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import { ErrorDisplay as UiErrorDisplay } from '@/ui/ErrorDisplay';
|
||||
import { ErrorDisplay as UiErrorDisplay } from '@/components/shared/ErrorDisplay';
|
||||
|
||||
interface ErrorDisplayProps {
|
||||
error: ApiError;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { ErrorPageContainer } from '@/ui/ErrorPageContainer';
|
||||
import { Glow } from '@/ui/Glow';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { FooterSection } from '@/ui/FooterSection';
|
||||
import { Group } from '@/ui/Group';
|
||||
import { NotFoundActions } from './NotFoundActions';
|
||||
import { NotFoundDiagnostics } from './NotFoundDiagnostics';
|
||||
import { NotFoundHelpLinks } from './NotFoundHelpLinks';
|
||||
@@ -42,40 +42,40 @@ export function NotFoundScreen({
|
||||
{/* Background Glow Accent */}
|
||||
<Glow color="primary" size="xl" opacity={0.1} position="center" />
|
||||
|
||||
<NotFoundDiagnostics errorCode={errorCode} />
|
||||
|
||||
<Text
|
||||
as="h1"
|
||||
size="4xl"
|
||||
weight="bold"
|
||||
variant="high"
|
||||
uppercase
|
||||
block
|
||||
align="center"
|
||||
style={{ marginTop: '1rem', marginBottom: '2rem' }}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<Group direction="column" align="center" gap={8} fullWidth>
|
||||
<NotFoundDiagnostics errorCode={errorCode} />
|
||||
|
||||
<Group direction="column" align="center" gap={4} fullWidth>
|
||||
<Text
|
||||
as="h1"
|
||||
size="4xl"
|
||||
weight="bold"
|
||||
variant="high"
|
||||
uppercase
|
||||
block
|
||||
align="center"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
size="xl"
|
||||
variant="med"
|
||||
block
|
||||
weight="medium"
|
||||
align="center"
|
||||
style={{ marginBottom: '3rem' }}
|
||||
>
|
||||
{message}
|
||||
</Text>
|
||||
<Text
|
||||
size="xl"
|
||||
variant="med"
|
||||
block
|
||||
weight="medium"
|
||||
align="center"
|
||||
>
|
||||
{message}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<NotFoundActions
|
||||
primaryLabel={actionLabel}
|
||||
onPrimaryClick={onActionClick}
|
||||
/>
|
||||
<NotFoundActions
|
||||
primaryLabel={actionLabel}
|
||||
onPrimaryClick={onActionClick}
|
||||
/>
|
||||
|
||||
<FooterSection>
|
||||
<NotFoundHelpLinks links={helpLinks} />
|
||||
</FooterSection>
|
||||
</Group>
|
||||
</ErrorPageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
57
apps/website/components/home/FAQSection.tsx
Normal file
57
apps/website/components/home/FAQSection.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { Section } from '@/ui/Section';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Accordion } from '@/components/shared/Accordion';
|
||||
import { CardStack } from '@/ui/CardStack';
|
||||
import { Center } from '@/ui/Center';
|
||||
|
||||
interface FAQItem {
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
interface FAQSectionProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
faqs: FAQItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* FAQSection - A semantic component for the FAQ section.
|
||||
*/
|
||||
export function FAQSection({
|
||||
title,
|
||||
subtitle,
|
||||
faqs,
|
||||
}: FAQSectionProps) {
|
||||
return (
|
||||
<Section variant="dark" padding="lg">
|
||||
<Container size="md">
|
||||
<Center>
|
||||
<CardStack gap={4}>
|
||||
<Center>
|
||||
<Text size="xs" weight="bold" variant="primary" uppercase letterSpacing="widest">
|
||||
{subtitle}
|
||||
</Text>
|
||||
</Center>
|
||||
<Heading level={2} weight="bold" align="center">
|
||||
{title}
|
||||
</Heading>
|
||||
</CardStack>
|
||||
</Center>
|
||||
|
||||
<CardStack gap={4}>
|
||||
{faqs.map((faq, index) => (
|
||||
<Accordion key={index} title={faq.question}>
|
||||
<Text size="sm" variant="low" leading="relaxed">
|
||||
{faq.answer}
|
||||
</Text>
|
||||
</Accordion>
|
||||
))}
|
||||
</CardStack>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { CardStack } from '@/ui/CardStack';
|
||||
import { FeatureList } from '@/ui/FeatureList';
|
||||
import { FeatureQuote } from '@/ui/FeatureQuote';
|
||||
|
||||
interface HomeFeatureDescriptionProps {
|
||||
lead: string;
|
||||
@@ -12,7 +14,7 @@ interface HomeFeatureDescriptionProps {
|
||||
|
||||
/**
|
||||
* HomeFeatureDescription - A semantic component for feature descriptions on the home page.
|
||||
* Refactored to use semantic HTML and Tailwind.
|
||||
* Refactored to use semantic UI components.
|
||||
*/
|
||||
export function HomeFeatureDescription({
|
||||
lead,
|
||||
@@ -20,40 +22,21 @@ export function HomeFeatureDescription({
|
||||
quote,
|
||||
accentColor = 'primary',
|
||||
}: HomeFeatureDescriptionProps) {
|
||||
const borderColor = {
|
||||
primary: 'primary-accent',
|
||||
aqua: 'telemetry-aqua',
|
||||
amber: 'warning-amber',
|
||||
gray: 'border-gray',
|
||||
}[accentColor];
|
||||
|
||||
const bgColor = {
|
||||
primary: 'primary-accent/5',
|
||||
aqua: 'telemetry-aqua/5',
|
||||
amber: 'warning-amber/5',
|
||||
gray: 'white/5',
|
||||
}[accentColor];
|
||||
const intent = accentColor === 'gray' ? 'low' : accentColor;
|
||||
|
||||
return (
|
||||
<Stack gap={6}>
|
||||
<Text size="lg" color="text-gray-400" weight="medium" leading="relaxed">
|
||||
<CardStack gap={6}>
|
||||
<Text size="lg" variant="med" leading="relaxed">
|
||||
{lead}
|
||||
</Text>
|
||||
<Stack as="ul" gap={2}>
|
||||
{items.map((item, index) => (
|
||||
<Stack as="li" key={index} direction="row" align="start" gap={2}>
|
||||
<Text color="text-primary-accent">•</Text>
|
||||
<Text size="sm" color="text-gray-500">{item}</Text>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
<FeatureList items={items} intent={intent} />
|
||||
|
||||
{quote && (
|
||||
<Stack borderLeft borderStyle="solid" borderWidth="2px" borderColor={borderColor} pl={4} py={1} bg={bgColor}>
|
||||
<Text color="text-gray-600" font="mono" size="xs" uppercase letterSpacing="widest" leading="relaxed">
|
||||
{quote}
|
||||
</Text>
|
||||
</Stack>
|
||||
<FeatureQuote intent={intent}>
|
||||
{quote}
|
||||
</FeatureQuote>
|
||||
)}
|
||||
</Stack>
|
||||
</CardStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Panel } from '@/ui/Panel';
|
||||
import { Glow } from '@/ui/Glow';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Section } from '@/ui/Section';
|
||||
import { FeatureSection } from '@/ui/FeatureSection';
|
||||
|
||||
interface HomeFeatureSectionProps {
|
||||
heading: string;
|
||||
@@ -27,43 +21,13 @@ export function HomeFeatureSection({
|
||||
layout,
|
||||
accentColor = 'primary',
|
||||
}: HomeFeatureSectionProps) {
|
||||
const glowColor = ({
|
||||
primary: 'primary',
|
||||
aqua: 'aqua',
|
||||
amber: 'amber',
|
||||
}[accentColor] || 'primary') as 'primary' | 'aqua' | 'amber' | 'purple';
|
||||
|
||||
return (
|
||||
<Section variant="dark" padding="lg">
|
||||
<Glow
|
||||
color={glowColor}
|
||||
size="lg"
|
||||
position={layout === 'text-left' ? 'bottom-left' : 'top-right'}
|
||||
opacity={0.02}
|
||||
/>
|
||||
|
||||
<Container>
|
||||
<Grid cols={{ base: 1, lg: 2 }} gap={12}>
|
||||
{/* Text Content */}
|
||||
<Stack gap={8}>
|
||||
<Stack gap={4}>
|
||||
<Heading level={2} weight="bold">
|
||||
{heading}
|
||||
</Heading>
|
||||
</Stack>
|
||||
<Stack>
|
||||
{description}
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Mockup Panel */}
|
||||
<Panel variant="dark">
|
||||
<Stack align="center" justify="center">
|
||||
{mockup}
|
||||
</Stack>
|
||||
</Panel>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Section>
|
||||
<FeatureSection
|
||||
heading={heading}
|
||||
description={description}
|
||||
mockup={mockup}
|
||||
layout={layout}
|
||||
intent={accentColor}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,109 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Glow } from '@/ui/Glow';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { DiscordIcon } from '@/ui/icons/DiscordIcon';
|
||||
import { Code, Lightbulb, LucideIcon, MessageSquare, Users } from 'lucide-react';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Section } from '@/ui/Section';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { DiscordCTA } from '@/ui/DiscordCTA';
|
||||
import { Code, Lightbulb, MessageSquare, Users } from 'lucide-react';
|
||||
|
||||
export function HomeFooterCTA() {
|
||||
const discordUrl = process.env.NEXT_PUBLIC_DISCORD_URL || '#';
|
||||
|
||||
const benefits = [
|
||||
{
|
||||
icon: MessageSquare,
|
||||
title: "Share your pain points",
|
||||
description: "Tell us what frustrates you about league racing today."
|
||||
},
|
||||
{
|
||||
icon: Lightbulb,
|
||||
title: "Shape the product",
|
||||
description: "Your ideas directly influence our roadmap."
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: "Connect with racers",
|
||||
description: "Join a community of like-minded competitive drivers."
|
||||
},
|
||||
{
|
||||
icon: Code,
|
||||
title: "Early Access",
|
||||
description: "Test new features before they go public."
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Section variant="dark" padding="lg">
|
||||
<Glow color="primary" size="xl" position="center" opacity={0.05} />
|
||||
|
||||
<Container>
|
||||
<Card variant="outline">
|
||||
<Stack align="center" gap={12}>
|
||||
{/* Header */}
|
||||
<Stack align="center" gap={6}>
|
||||
<DiscordIcon size={40} />
|
||||
|
||||
<Stack gap={4} align="center">
|
||||
<Heading level={2} weight="bold" align="center">
|
||||
Join the Grid on Discord
|
||||
</Heading>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Personal message */}
|
||||
<Stack align="center" gap={6}>
|
||||
<Text size="lg" variant="high" weight="medium" align="center">
|
||||
GridPilot is a <Text as="span" variant="high" weight="bold">solo developer project</Text> built for the community.
|
||||
</Text>
|
||||
<Text size="base" variant="low" align="center">
|
||||
We are in early alpha. Join us to help shape the future of motorsport infrastructure. Your feedback directly influences the roadmap.
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
{/* Benefits grid */}
|
||||
<Grid cols={{ base: 1, md: 2 }} gap={6}>
|
||||
<BenefitItem
|
||||
icon={MessageSquare}
|
||||
title="Share your pain points"
|
||||
description="Tell us what frustrates you about league racing today."
|
||||
/>
|
||||
<BenefitItem
|
||||
icon={Lightbulb}
|
||||
title="Shape the product"
|
||||
description="Your ideas directly influence our roadmap."
|
||||
/>
|
||||
<BenefitItem
|
||||
icon={Users}
|
||||
title="Connect with racers"
|
||||
description="Join a community of like-minded competitive drivers."
|
||||
/>
|
||||
<BenefitItem
|
||||
icon={Code}
|
||||
title="Early Access"
|
||||
description="Test new features before they go public."
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* CTA Button */}
|
||||
<Stack gap={6} align="center">
|
||||
<Button
|
||||
as="a"
|
||||
href={discordUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
icon={<DiscordIcon size={24} />}
|
||||
>
|
||||
Join Discord
|
||||
</Button>
|
||||
|
||||
<Text size="xs" variant="primary" weight="bold" font="mono" uppercase>
|
||||
Early Alpha Access Available
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
function BenefitItem({ icon, title, description }: { icon: LucideIcon, title: string, description: string }) {
|
||||
return (
|
||||
<Card variant="dark">
|
||||
<Stack align="start" gap={5}>
|
||||
<Icon icon={icon} size={5} intent="primary" />
|
||||
<Stack gap={2}>
|
||||
<Text size="base" weight="bold" variant="high">{title}</Text>
|
||||
<Text size="sm" variant="low">{description}</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
<DiscordCTA
|
||||
title="Join the Grid on Discord"
|
||||
lead="GridPilot is a solo developer project built for the community."
|
||||
description="We are in early alpha. Join us to help shape the future of motorsport infrastructure. Your feedback directly influences the roadmap."
|
||||
benefits={benefits}
|
||||
discordUrl={discordUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Glow } from '@/ui/Glow';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Section } from '@/ui/Section';
|
||||
import { ButtonGroup } from '@/ui/ButtonGroup';
|
||||
import { LandingHero } from '@/ui/LandingHero';
|
||||
|
||||
interface HomeHeaderProps {
|
||||
title: string;
|
||||
@@ -26,51 +19,6 @@ interface HomeHeaderProps {
|
||||
/**
|
||||
* HomeHeader - Semantic hero section for the landing page.
|
||||
*/
|
||||
export function HomeHeader({
|
||||
title,
|
||||
subtitle,
|
||||
description,
|
||||
primaryAction,
|
||||
secondaryAction,
|
||||
}: HomeHeaderProps) {
|
||||
return (
|
||||
<Section variant="dark" padding="lg">
|
||||
<Glow color="primary" size="xl" position="top-right" opacity={0.1} />
|
||||
|
||||
<Container>
|
||||
<Stack gap={8}>
|
||||
<Text size="xs" weight="bold" uppercase variant="primary">
|
||||
{subtitle}
|
||||
</Text>
|
||||
|
||||
<Heading level={1} weight="bold">
|
||||
{title}
|
||||
</Heading>
|
||||
|
||||
<Text size="lg" variant="low">
|
||||
{description}
|
||||
</Text>
|
||||
|
||||
<ButtonGroup gap={4}>
|
||||
<Button
|
||||
as="a"
|
||||
href={primaryAction.href}
|
||||
variant="primary"
|
||||
size="lg"
|
||||
>
|
||||
{primaryAction.label}
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href={secondaryAction.href}
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
>
|
||||
{secondaryAction.label}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
export function HomeHeader(props: HomeHeaderProps) {
|
||||
return <LandingHero {...props} />;
|
||||
}
|
||||
|
||||
@@ -1,43 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { MetricCard } from '@/ui/MetricCard';
|
||||
import { StatsStrip } from '@/ui/StatsStrip';
|
||||
import { Activity, Users, Trophy, Calendar } from 'lucide-react';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
|
||||
/**
|
||||
* HomeStatsStrip - A thin strip showing some status or quick info.
|
||||
* Part of the "Telemetry-workspace" feel.
|
||||
*/
|
||||
export function HomeStatsStrip() {
|
||||
return (
|
||||
<Container>
|
||||
<Grid cols={{ base: 2, md: 4 }} gap={4}>
|
||||
<MetricCard
|
||||
label="Active Drivers"
|
||||
value="1,284"
|
||||
icon={Users}
|
||||
trend={{ value: 12, isPositive: true }}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Live Sessions"
|
||||
value="42"
|
||||
icon={Activity}
|
||||
intent="telemetry"
|
||||
/>
|
||||
<MetricCard
|
||||
label="Total Races"
|
||||
value="15,402"
|
||||
icon={Trophy}
|
||||
intent="warning"
|
||||
/>
|
||||
<MetricCard
|
||||
label="Next Event"
|
||||
value="14:00"
|
||||
icon={Calendar}
|
||||
/>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
const stats = [
|
||||
{
|
||||
label: "Active Drivers",
|
||||
value: "1,284",
|
||||
icon: Users,
|
||||
trend: { value: 12, isPositive: true }
|
||||
},
|
||||
{
|
||||
label: "Live Sessions",
|
||||
value: "42",
|
||||
icon: Activity,
|
||||
intent: "telemetry" as const
|
||||
},
|
||||
{
|
||||
label: "Total Races",
|
||||
value: "15,402",
|
||||
icon: Trophy,
|
||||
intent: "warning" as const
|
||||
},
|
||||
{
|
||||
label: "Next Event",
|
||||
value: "14:00",
|
||||
icon: Calendar
|
||||
},
|
||||
];
|
||||
|
||||
return <StatsStrip stats={stats} />;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
import { LeagueCard } from '@/components/leagues/LeagueCard';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Panel } from '@/ui/Panel';
|
||||
import { Box } from '@/ui/Box';
|
||||
|
||||
import { CardStack } from '@/ui/CardStack';
|
||||
|
||||
interface League {
|
||||
id: string;
|
||||
@@ -20,26 +23,26 @@ interface LeagueSummaryPanelProps {
|
||||
* LeagueSummaryPanel - Semantic section for featured leagues.
|
||||
*/
|
||||
export function LeagueSummaryPanel({ leagues }: LeagueSummaryPanelProps) {
|
||||
return (
|
||||
<Box as="section" bg="surface-charcoal" p={6} border borderColor="border-gray" rounded="none">
|
||||
<Box display="flex" alignItems="center" justifyContent="between" mb={6}>
|
||||
<Heading level={3} fontSize="xs" weight="bold" letterSpacing="widest" color="text-white">
|
||||
FEATURED LEAGUES
|
||||
</Heading>
|
||||
<Link
|
||||
href={routes.public.leagues}
|
||||
size="xs"
|
||||
weight="bold"
|
||||
letterSpacing="widest"
|
||||
variant="primary"
|
||||
hoverColor="text-white"
|
||||
transition
|
||||
>
|
||||
VIEW ALL →
|
||||
</Link>
|
||||
</Box>
|
||||
const actions = (
|
||||
<Link
|
||||
href={routes.public.leagues}
|
||||
size="xs"
|
||||
weight="bold"
|
||||
letterSpacing="widest"
|
||||
variant="primary"
|
||||
>
|
||||
VIEW ALL →
|
||||
</Link>
|
||||
);
|
||||
|
||||
<Box display="flex" flexDirection="col" gap={4}>
|
||||
return (
|
||||
<Panel
|
||||
variant="dark"
|
||||
padding={6}
|
||||
title="FEATURED LEAGUES"
|
||||
actions={actions}
|
||||
>
|
||||
<CardStack gap={4}>
|
||||
{leagues.slice(0, 2).map((league) => (
|
||||
<LeagueCard
|
||||
key={league.id}
|
||||
@@ -54,7 +57,7 @@ export function LeagueSummaryPanel({ leagues }: LeagueSummaryPanelProps) {
|
||||
openSlotsCount={6}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</CardStack>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,9 +5,13 @@ import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Panel } from '@/ui/Panel';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
|
||||
import { CardStack } from '@/ui/CardStack';
|
||||
|
||||
import { Center } from '@/ui/Center';
|
||||
|
||||
interface Race {
|
||||
id: string;
|
||||
track: string;
|
||||
@@ -23,33 +27,33 @@ interface RecentRacesPanelProps {
|
||||
* RecentRacesPanel - Semantic section for upcoming/recent races.
|
||||
*/
|
||||
export function RecentRacesPanel({ races }: RecentRacesPanelProps) {
|
||||
return (
|
||||
<Panel variant="dark" padding={6}>
|
||||
<Stack direction="row" align="center" justify="between" mb={6}>
|
||||
<Heading level={3} fontSize="xs" weight="bold" letterSpacing="widest" color="text-white">
|
||||
UPCOMING RACES
|
||||
</Heading>
|
||||
<Link
|
||||
href={routes.public.races}
|
||||
size="xs"
|
||||
weight="bold"
|
||||
letterSpacing="widest"
|
||||
variant="primary"
|
||||
hoverColor="text-white"
|
||||
transition
|
||||
>
|
||||
FULL SCHEDULE →
|
||||
</Link>
|
||||
</Stack>
|
||||
const actions = (
|
||||
<Link
|
||||
href={routes.public.races}
|
||||
size="xs"
|
||||
weight="bold"
|
||||
letterSpacing="widest"
|
||||
variant="primary"
|
||||
>
|
||||
FULL SCHEDULE →
|
||||
</Link>
|
||||
);
|
||||
|
||||
<Stack gap={3}>
|
||||
return (
|
||||
<Panel
|
||||
variant="dark"
|
||||
padding={6}
|
||||
title="UPCOMING RACES"
|
||||
actions={actions}
|
||||
>
|
||||
<CardStack gap={3}>
|
||||
{races.length === 0 ? (
|
||||
<Panel variant="muted" padding={12} border>
|
||||
<Stack center>
|
||||
<Text size="xs" font="mono" uppercase letterSpacing="widest" color="text-gray-600">
|
||||
<Center>
|
||||
<Text size="xs" font="mono" uppercase letterSpacing="widest" variant="low">
|
||||
No races scheduled
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Panel>
|
||||
) : (
|
||||
races.slice(0, 3).map((race) => (
|
||||
@@ -63,7 +67,7 @@ export function RecentRacesPanel({ races }: RecentRacesPanelProps) {
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
</CardStack>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
import { TeamCard } from '@/components/teams/TeamCard';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Panel } from '@/ui/Panel';
|
||||
import { Box } from '@/ui/Box';
|
||||
|
||||
import { CardStack } from '@/ui/CardStack';
|
||||
|
||||
interface Team {
|
||||
id: string;
|
||||
@@ -21,26 +24,26 @@ interface TeamSummaryPanelProps {
|
||||
* TeamSummaryPanel - Semantic section for teams.
|
||||
*/
|
||||
export function TeamSummaryPanel({ teams }: TeamSummaryPanelProps) {
|
||||
return (
|
||||
<Box as="section" bg="surface-charcoal" p={6} border borderColor="border-gray" rounded="none">
|
||||
<Box display="flex" alignItems="center" justifyContent="between" mb={6}>
|
||||
<Heading level={3} fontSize="xs" weight="bold" letterSpacing="widest" color="text-white">
|
||||
TEAMS ON THE GRID
|
||||
</Heading>
|
||||
<Link
|
||||
href={routes.public.teams}
|
||||
size="xs"
|
||||
weight="bold"
|
||||
letterSpacing="widest"
|
||||
variant="primary"
|
||||
hoverColor="text-white"
|
||||
transition
|
||||
>
|
||||
BROWSE TEAMS →
|
||||
</Link>
|
||||
</Box>
|
||||
const actions = (
|
||||
<Link
|
||||
href={routes.public.teams}
|
||||
size="xs"
|
||||
weight="bold"
|
||||
letterSpacing="widest"
|
||||
variant="primary"
|
||||
>
|
||||
BROWSE TEAMS →
|
||||
</Link>
|
||||
);
|
||||
|
||||
<Box display="flex" flexDirection="col" gap={4}>
|
||||
return (
|
||||
<Panel
|
||||
variant="dark"
|
||||
padding={6}
|
||||
title="TEAMS ON THE GRID"
|
||||
actions={actions}
|
||||
>
|
||||
<CardStack gap={4}>
|
||||
{teams.slice(0, 2).map((team) => (
|
||||
<TeamCard
|
||||
key={team.id}
|
||||
@@ -51,7 +54,7 @@ export function TeamSummaryPanel({ teams }: TeamSummaryPanelProps) {
|
||||
isRecruiting={true}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</CardStack>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,8 +44,13 @@ export function BenefitCard({
|
||||
rounded="xl"
|
||||
border={true}
|
||||
padding={6}
|
||||
className={`relative h-full transition-all duration-300 group ${isHighlight ? 'border-primary-blue/30' : 'border-charcoal-outline hover:border-charcoal-outline/80'}`}
|
||||
style={isHighlight ? { background: 'linear-gradient(to bottom right, rgba(25, 140, 255, 0.1), rgba(25, 140, 255, 0.05))' } : {}}
|
||||
borderColor={isHighlight ? 'rgba(25, 140, 255, 0.3)' : 'var(--ui-color-border-low)'}
|
||||
hoverBorderColor={isHighlight ? 'rgba(25, 140, 255, 0.5)' : 'var(--ui-color-border-default)'}
|
||||
transition="all 0.3s ease"
|
||||
group
|
||||
position="relative"
|
||||
fullHeight
|
||||
bg={isHighlight ? 'linear-gradient(to bottom right, rgba(25, 140, 255, 0.1), rgba(25, 140, 255, 0.05))' : undefined}
|
||||
>
|
||||
{/* Icon */}
|
||||
<Box
|
||||
@@ -55,25 +60,25 @@ export function BenefitCard({
|
||||
display="flex"
|
||||
center
|
||||
mb={4}
|
||||
bg={isHighlight ? 'bg-primary-blue/20' : 'bg-iron-gray'}
|
||||
bg={isHighlight ? 'rgba(25, 140, 255, 0.2)' : 'var(--ui-color-bg-surface-muted)'}
|
||||
border={!isHighlight}
|
||||
borderColor="border-charcoal-outline"
|
||||
borderColor="var(--ui-color-border-low)"
|
||||
>
|
||||
<Icon icon={icon} size={6} className={isHighlight ? 'text-primary-blue' : 'text-gray-400'} />
|
||||
<Icon icon={icon} size={6} intent={isHighlight ? 'primary' : 'low'} />
|
||||
</Box>
|
||||
|
||||
{/* Content */}
|
||||
<Heading level={3} mb={2}>{title}</Heading>
|
||||
<Text size="sm" color="text-gray-400" block style={{ lineHeight: 1.625 }}>{description}</Text>
|
||||
<Text size="sm" variant="low" block leading="relaxed">{description}</Text>
|
||||
|
||||
{/* Stats */}
|
||||
{stats && (
|
||||
<Box mt={4} pt={4} borderTop={true} borderColor="border-charcoal-outline/50">
|
||||
<Box mt={4} pt={4} borderTop={true} borderColor="var(--ui-color-border-low)">
|
||||
<Box display="flex" alignItems="baseline" gap={2}>
|
||||
<Text size="2xl" weight="bold" color={isHighlight ? 'text-primary-blue' : 'text-white'}>
|
||||
<Text size="2xl" weight="bold" variant={isHighlight ? 'primary' : 'high'}>
|
||||
{stats.value}
|
||||
</Text>
|
||||
<Text size="sm" color="text-gray-500">{stats.label}</Text>
|
||||
<Text size="sm" variant="low">{stats.label}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
@@ -84,7 +89,11 @@ export function BenefitCard({
|
||||
position="absolute"
|
||||
inset="0"
|
||||
rounded="xl"
|
||||
className="bg-gradient-to-br from-primary-blue/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"
|
||||
bg="linear-gradient(to bottom right, rgba(25, 140, 255, 0.2), transparent)"
|
||||
opacity={0}
|
||||
groupHoverOpacity={1}
|
||||
transition="opacity 0.3s ease"
|
||||
pointerEvents="none"
|
||||
/>
|
||||
)}
|
||||
</Surface>
|
||||
|
||||
@@ -24,11 +24,11 @@ export function DiscoverySection({ viewData }: DiscoverySectionProps) {
|
||||
<Stack gap={16}>
|
||||
<Stack maxWidth="2xl">
|
||||
<Stack borderLeft borderStyle="solid" borderColor="primary-accent" pl={4} mb={4}>
|
||||
<Text size="xs" weight="bold" color="text-primary-accent" className="uppercase tracking-[0.2em]">
|
||||
<Text size="xs" weight="bold" variant="primary" uppercase letterSpacing="0.2em">
|
||||
Live Ecosystem
|
||||
</Text>
|
||||
</Stack>
|
||||
<Heading level={2} weight="bold" fontSize={{ base: '3xl', md: '4xl' }} className="tracking-tight">
|
||||
<Heading level={2} weight="bold" fontSize={{ base: '3xl', md: '4xl' }} letterSpacing="-0.025em">
|
||||
Discover the Grid
|
||||
</Heading>
|
||||
<Text size="lg" color="text-gray-400" block mt={6} leading="relaxed">
|
||||
@@ -39,10 +39,10 @@ export function DiscoverySection({ viewData }: DiscoverySectionProps) {
|
||||
<Grid cols={1} lgCols={3} gap={12}>
|
||||
{/* Top leagues */}
|
||||
<Stack gap={8}>
|
||||
<Stack direction="row" align="center" justify="between" className="border-b border-border-gray/30 pb-4">
|
||||
<Heading level={5} color="text-gray-400" weight="bold" className="tracking-widest">FEATURED LEAGUES</Heading>
|
||||
<Stack direction="row" align="center" justify="between" borderBottom borderColor="var(--ui-color-border-low)" pb={4}>
|
||||
<Heading level={5} color="var(--ui-color-text-low)" weight="bold" letterSpacing="widest" uppercase>FEATURED LEAGUES</Heading>
|
||||
<Link href={routes.public.leagues}>
|
||||
<Text size="xs" weight="bold" color="text-primary-accent" className="uppercase tracking-widest hover:text-white transition-colors">View all</Text>
|
||||
<Text size="xs" weight="bold" variant="primary" uppercase letterSpacing="widest" hoverVariant="high">View all</Text>
|
||||
</Link>
|
||||
</Stack>
|
||||
<Stack gap={4}>
|
||||
@@ -65,10 +65,10 @@ export function DiscoverySection({ viewData }: DiscoverySectionProps) {
|
||||
|
||||
{/* Teams */}
|
||||
<Stack gap={8}>
|
||||
<Stack direction="row" align="center" justify="between" className="border-b border-border-gray/30 pb-4">
|
||||
<Heading level={5} color="text-gray-400" weight="bold" className="tracking-widest">TEAMS ON THE GRID</Heading>
|
||||
<Stack direction="row" align="center" justify="between" borderBottom borderColor="var(--ui-color-border-low)" pb={4}>
|
||||
<Heading level={5} color="var(--ui-color-text-low)" weight="bold" letterSpacing="widest" uppercase>TEAMS ON THE GRID</Heading>
|
||||
<Link href={routes.public.teams}>
|
||||
<Text size="xs" weight="bold" color="text-primary-accent" className="uppercase tracking-widest hover:text-white transition-colors">Browse</Text>
|
||||
<Text size="xs" weight="bold" variant="primary" uppercase letterSpacing="widest" hoverVariant="high">Browse</Text>
|
||||
</Link>
|
||||
</Stack>
|
||||
<Stack gap={4}>
|
||||
@@ -87,10 +87,10 @@ export function DiscoverySection({ viewData }: DiscoverySectionProps) {
|
||||
|
||||
{/* Upcoming races */}
|
||||
<Stack gap={8}>
|
||||
<Stack direction="row" align="center" justify="between" className="border-b border-border-gray/30 pb-4">
|
||||
<Heading level={5} color="text-gray-400" weight="bold" className="tracking-widest">UPCOMING RACES</Heading>
|
||||
<Stack direction="row" align="center" justify="between" borderBottom borderColor="var(--ui-color-border-low)" pb={4}>
|
||||
<Heading level={5} color="var(--ui-color-text-low)" weight="bold" letterSpacing="widest" uppercase>UPCOMING RACES</Heading>
|
||||
<Link href={routes.public.races}>
|
||||
<Text size="xs" weight="bold" color="text-primary-accent" className="uppercase tracking-widest hover:text-white transition-colors">Schedule</Text>
|
||||
<Text size="xs" weight="bold" variant="primary" uppercase letterSpacing="widest" hoverVariant="high">Schedule</Text>
|
||||
</Link>
|
||||
</Stack>
|
||||
{viewData.upcomingRaces.length === 0 ? (
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { FAQSection } from '@/components/home/FAQSection';
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
@@ -35,103 +29,12 @@ const faqs = [
|
||||
}
|
||||
];
|
||||
|
||||
function FAQItem({ faq, index }: { faq: typeof faqs[0]; index: number }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
as={motion.div}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
group
|
||||
>
|
||||
<Stack rounded="none" bg="panel-gray/40" border borderColor="border-gray/50" transition hoverBorderColor="primary-accent/30">
|
||||
<Stack
|
||||
as="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
fullWidth
|
||||
p={{ base: 4, md: 6 }}
|
||||
textAlign="left"
|
||||
rounded="none"
|
||||
minHeight="44px"
|
||||
className="relative overflow-hidden"
|
||||
>
|
||||
<Stack display="flex" alignItems="center" justifyContent="between" gap={{ base: 2, md: 4 }}>
|
||||
<Stack direction="row" align="center" gap={4}>
|
||||
<Stack w="1" h="4" bg={isOpen ? "primary-accent" : "border-gray"} transition className="group-hover:bg-primary-accent" />
|
||||
<Heading level={3} fontSize={{ base: 'sm', md: 'base' }} weight="bold" color="text-white" groupHoverColor="primary-accent" transition className="tracking-wide">
|
||||
{faq.question}
|
||||
</Heading>
|
||||
</Stack>
|
||||
<Stack
|
||||
as={motion.div}
|
||||
animate={{ rotate: isOpen ? 180 : 0 }}
|
||||
transition={{ duration: 0.15, ease: 'easeInOut' }}
|
||||
w={{ base: "4", md: "5" }}
|
||||
h={{ base: "4", md: "5" }}
|
||||
color={isOpen ? "text-primary-accent" : "text-gray-500"}
|
||||
flexShrink={0}
|
||||
>
|
||||
<Icon icon={ChevronDown} size="full" />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
as={motion.div}
|
||||
initial={false}
|
||||
animate={{
|
||||
height: isOpen ? 'auto' : 0,
|
||||
opacity: isOpen ? 1 : 0
|
||||
}}
|
||||
transition={{
|
||||
height: { duration: 0.2, ease: 'easeInOut' },
|
||||
opacity: { duration: 0.15, ease: 'easeInOut' }
|
||||
}}
|
||||
overflow="hidden"
|
||||
>
|
||||
<Stack px={{ base: 4, md: 6 }} pb={{ base: 4, md: 6 }} pt={0} ml={5}>
|
||||
<Text size="sm" color="text-gray-400" weight="normal" leading="relaxed" className="max-w-2xl">
|
||||
{faq.answer}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export function FAQ() {
|
||||
return (
|
||||
<Stack as="section" position="relative" py={{ base: 20, md: 32 }} bg="graphite-black" overflow="hidden" borderBottom borderColor="border-gray/50">
|
||||
{/* Background image with mask */}
|
||||
<Stack
|
||||
position="absolute"
|
||||
inset="0"
|
||||
bg="url(/images/porsche.jpeg)"
|
||||
backgroundSize="cover"
|
||||
backgroundPosition="center"
|
||||
opacity={0.03}
|
||||
/>
|
||||
|
||||
<Stack maxWidth="4xl" mx="auto" px={{ base: 4, md: 6 }} position="relative" zIndex={10}>
|
||||
<Stack textAlign="center" mb={{ base: 12, md: 16 }}>
|
||||
<Stack borderLeft borderRight borderStyle="solid" borderColor="primary-accent" px={4} display="inline-block" mb={4}>
|
||||
<Text size="xs" weight="bold" color="text-primary-accent" className="uppercase tracking-[0.2em]">
|
||||
Support & Information
|
||||
</Text>
|
||||
</Stack>
|
||||
<Heading level={2} fontSize={{ base: '3xl', md: '4xl' }} weight="bold" color="text-white" mb={4} className="tracking-tight">
|
||||
Frequently Asked Questions
|
||||
</Heading>
|
||||
</Stack>
|
||||
<Stack display="flex" flexDirection="column" gap={4}>
|
||||
{faqs.map((faq, index) => (
|
||||
<FAQItem key={faq.question} faq={faq} index={index} />
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<FAQSection
|
||||
title="Frequently Asked Questions"
|
||||
subtitle="Support & Information"
|
||||
faqs={faqs}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ import { Heading } from '@/ui/Heading';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Section } from '@/ui/Section';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
|
||||
const features = [
|
||||
{
|
||||
@@ -48,68 +51,79 @@ const features = [
|
||||
|
||||
function FeatureCard({ feature, index }: { feature: typeof features[0], index: number }) {
|
||||
return (
|
||||
<Stack
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
gap={6}
|
||||
className="p-8 bg-panel-gray/20 border border-border-gray/20 rounded-none hover:border-primary-accent/20 transition-all duration-300 ease-smooth group relative overflow-hidden"
|
||||
<Surface
|
||||
variant="muted"
|
||||
padding={8}
|
||||
rounded="none"
|
||||
border
|
||||
borderColor="var(--ui-color-border-low)"
|
||||
hoverBorderColor="var(--ui-color-intent-primary)"
|
||||
transition="all 0.3s ease"
|
||||
group
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Stack aspectRatio="video" fullWidth position="relative" className="bg-graphite-black rounded-none overflow-hidden border border-border-gray/20">
|
||||
<Stack aspectRatio="video" fullWidth position="relative" bg="var(--ui-color-bg-base)" rounded="none" overflow="hidden" border borderColor="var(--ui-color-border-low)">
|
||||
<MockupStack index={index}>
|
||||
<feature.MockupComponent />
|
||||
</MockupStack>
|
||||
</Stack>
|
||||
<Stack gap={4}>
|
||||
<Stack display="flex" alignItems="center" gap={3}>
|
||||
<Stack w="1" h="3" bg="primary-accent" />
|
||||
<Heading level={3} weight="bold" fontSize="lg" className="tracking-tighter uppercase">
|
||||
<Stack gap={4} mt={6}>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Box w="1" h="3" bg="var(--ui-color-intent-primary)" />
|
||||
<Heading level={3} weight="bold" fontSize="lg" letterSpacing="-0.05em" uppercase>
|
||||
{feature.title}
|
||||
</Heading>
|
||||
</Stack>
|
||||
<Text size="sm" color="text-gray-500" leading="relaxed" weight="normal" className="group-hover:text-gray-400 transition-colors">
|
||||
<Text size="sm" variant="low" leading="relaxed" weight="normal" groupHoverTextColor="var(--ui-color-text-med)">
|
||||
{feature.description}
|
||||
</Text>
|
||||
</Stack>
|
||||
{/* Subtle hover effect */}
|
||||
<Stack
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom="0"
|
||||
left="0"
|
||||
w="full"
|
||||
h="0.5"
|
||||
bg="primary-accent"
|
||||
className="scale-x-0 group-hover:scale-x-100 transition-transform duration-500 origin-left"
|
||||
bg="var(--ui-color-intent-primary)"
|
||||
transform="scaleX(0)"
|
||||
groupHoverScale={true}
|
||||
transition="transform 0.5s ease"
|
||||
style={{ transformOrigin: 'left' }}
|
||||
/>
|
||||
</Stack>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
|
||||
export function FeatureGrid() {
|
||||
return (
|
||||
<Section className="bg-graphite-black border-b border-border-gray/20 py-32">
|
||||
<Container position="relative" zIndex={10}>
|
||||
<Box borderBottom borderColor="var(--ui-color-border-low)">
|
||||
<Section variant="dark" py={32}>
|
||||
<Container position="relative" zIndex={10}>
|
||||
<Stack gap={16}>
|
||||
<Stack maxWidth="2xl">
|
||||
<Stack borderLeft borderStyle="solid" borderColor="primary-accent" pl={4} mb={4} bg="primary-accent/5" py={1}>
|
||||
<Text size="xs" weight="bold" color="text-primary-accent" className="uppercase tracking-[0.3em]">
|
||||
<Stack borderLeft borderStyle="solid" borderColor="var(--ui-color-intent-primary)" pl={4} mb={4} bg="rgba(25, 140, 255, 0.05)" py={1}>
|
||||
<Text size="xs" weight="bold" variant="primary" uppercase letterSpacing="0.3em">
|
||||
Engineered for Competition
|
||||
</Text>
|
||||
</Stack>
|
||||
<Heading level={2} weight="bold" fontSize={{ base: '3xl', md: '5xl' }} className="tracking-tighter uppercase leading-none">
|
||||
<Heading level={2} weight="bold" fontSize={{ base: '3xl', md: '5xl' }} letterSpacing="-0.05em" uppercase>
|
||||
Building for League Racing
|
||||
</Heading>
|
||||
<Text size="lg" color="text-gray-500" block mt={6} leading="relaxed" className="border-l border-border-gray/20 pl-6">
|
||||
<Text size="lg" variant="low" block mt={6} leading="relaxed" borderLeft borderColor="var(--ui-color-border-low)" pl={6}>
|
||||
Every feature is designed to reduce friction and increase immersion. Join our Discord to help shape the future of the platform.
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack display="grid" gridCols={{ base: 1, md: 2, lg: 3 }} gap={6}>
|
||||
<Grid cols={{ base: 1, md: 2, lg: 3 }} gap={6}>
|
||||
{features.map((feature, index) => (
|
||||
<FeatureCard key={feature.title} feature={feature} index={index} />
|
||||
))}
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</Section>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,10 +19,11 @@ export function LandingHero() {
|
||||
ref={sectionRef}
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
bg="graphite-black"
|
||||
bg="var(--ui-color-bg-base)"
|
||||
pt={{ base: 24, md: 32, lg: 40 }}
|
||||
pb={{ base: 16, md: 24, lg: 32 }}
|
||||
className="border-b border-border-gray"
|
||||
borderBottom
|
||||
borderColor="var(--ui-color-border-low)"
|
||||
>
|
||||
{/* Background image layer with parallax */}
|
||||
<Stack
|
||||
@@ -54,8 +55,8 @@ export function LandingHero() {
|
||||
<Container size="lg" position="relative" zIndex={10}>
|
||||
<Stack gap={{ base: 8, md: 12 }}>
|
||||
<Stack gap={6} maxWidth="3xl">
|
||||
<Stack borderLeft borderStyle="solid" borderColor="primary-accent" pl={4} mb={2} bg="primary-accent/5" py={1}>
|
||||
<Text size="xs" weight="bold" color="text-primary-accent" uppercase letterSpacing="0.3em">
|
||||
<Stack borderLeft borderStyle="solid" borderColor="var(--ui-color-intent-primary)" pl={4} mb={2} bg="rgba(25, 140, 255, 0.05)" py={1}>
|
||||
<Text size="xs" weight="bold" variant="primary" uppercase letterSpacing="0.3em">
|
||||
Precision Racing Infrastructure
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -63,13 +64,13 @@ export function LandingHero() {
|
||||
level={1}
|
||||
fontSize={{ base: '4xl', sm: '5xl', md: '6xl', lg: '8xl' }}
|
||||
weight="bold"
|
||||
color="text-white"
|
||||
color="var(--ui-color-text-high)"
|
||||
lineHeight="0.95"
|
||||
letterSpacing="tighter"
|
||||
>
|
||||
MODERN MOTORSPORT INFRASTRUCTURE.
|
||||
</Heading>
|
||||
<Text size={{ base: 'lg', md: 'xl' }} color="text-gray-400" weight="normal" leading="relaxed" maxWidth="2xl" borderLeft borderStyle="solid" borderColor="border-gray" pl={6} opacity={0.3}>
|
||||
<Text size={{ base: 'lg', md: 'xl' }} variant="low" weight="normal" leading="relaxed" maxWidth="2xl" borderLeft borderStyle="solid" borderColor="var(--ui-color-border-low)" pl={6} opacity={0.3}>
|
||||
GridPilot gives your league racing a real home. Results, standings, teams, and career progression — engineered for precision and control.
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -91,8 +92,8 @@ export function LandingHero() {
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
px={12}
|
||||
borderColor="border-gray"
|
||||
hoverBorderColor="primary-accent/50"
|
||||
borderColor="var(--ui-color-border-low)"
|
||||
hoverBorderColor="var(--ui-color-intent-primary)"
|
||||
letterSpacing="0.2em"
|
||||
fontSize="xs"
|
||||
h="14"
|
||||
@@ -110,24 +111,24 @@ export function LandingHero() {
|
||||
mt={12}
|
||||
borderTop
|
||||
borderStyle="solid"
|
||||
borderColor="border-gray"
|
||||
borderColor="var(--ui-color-border-low)"
|
||||
opacity={0.2}
|
||||
pt={12}
|
||||
>
|
||||
{[
|
||||
{ label: 'IDENTITY', text: 'Your racing career in one place', color: 'primary' },
|
||||
{ label: 'AUTOMATION', text: 'No more manual session setup', color: 'aqua' },
|
||||
{ label: 'PRECISION', text: 'Real-time results and standings', color: 'amber' },
|
||||
{ label: 'COMMUNITY', text: 'Built for teams and leagues', color: 'green' }
|
||||
{ label: 'IDENTITY', text: 'Your racing career in one place', color: 'var(--ui-color-intent-primary)' },
|
||||
{ label: 'AUTOMATION', text: 'No more manual session setup', color: 'var(--ui-color-intent-telemetry)' },
|
||||
{ label: 'PRECISION', text: 'Real-time results and standings', color: 'var(--ui-color-intent-warning)' },
|
||||
{ label: 'COMMUNITY', text: 'Built for teams and leagues', color: 'var(--ui-color-intent-success)' }
|
||||
].map((item) => (
|
||||
<Stack key={item.label} gap={3} group cursor="default">
|
||||
<Stack display="flex" alignItems="center" gap={3}>
|
||||
<Stack w="1" h="3" bg={item.color === 'primary' ? 'primary-accent' : item.color === 'aqua' ? 'telemetry-aqua' : item.color === 'amber' ? 'warning-amber' : 'success-green'} />
|
||||
<Text size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="0.2em" groupHoverTextColor="white" transition>
|
||||
<Stack w="1" h="3" bg={item.color} />
|
||||
<Text size="xs" weight="bold" variant="low" uppercase letterSpacing="0.2em" groupHoverTextColor="var(--ui-color-text-high)" transition>
|
||||
{item.label}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Text size="sm" color="text-gray-500" groupHoverTextColor="gray-300" transition leading="relaxed">
|
||||
<Text size="sm" variant="low" groupHoverTextColor="var(--ui-color-text-med)" transition leading="relaxed">
|
||||
{item.text}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -15,6 +15,7 @@ export function AuthedNav({ pathname, direction = 'col' }: AuthedNavProps) {
|
||||
const items = [
|
||||
{ label: 'Dashboard', href: routes.protected.dashboard, icon: Home },
|
||||
{ label: 'Leagues', href: routes.public.leagues, icon: Trophy },
|
||||
{ label: 'Drivers', href: routes.public.drivers, icon: Users },
|
||||
{ label: 'Leaderboards', href: routes.public.leaderboards, icon: Layout },
|
||||
{ label: 'Teams', href: routes.public.teams, icon: Users },
|
||||
{ label: 'Races', href: routes.public.races, icon: Calendar },
|
||||
|
||||
@@ -15,6 +15,7 @@ export function PublicNav({ pathname, direction = 'col' }: PublicNavProps) {
|
||||
const items = [
|
||||
{ label: 'Home', href: routes.public.home, icon: Home },
|
||||
{ label: 'Leagues', href: routes.public.leagues, icon: Trophy },
|
||||
{ label: 'Drivers', href: routes.public.drivers, icon: Users },
|
||||
{ label: 'Leaderboards', href: routes.public.leaderboards, icon: Layout },
|
||||
{ label: 'Teams', href: routes.public.teams, icon: Users },
|
||||
{ label: 'Races', href: routes.public.races, icon: Calendar },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { InfoBanner } from '@/ui/InfoBanner';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import { Modal } from '@/components/shared/Modal';
|
||||
import { ModalIcon } from '@/ui/ModalIcon';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useEffectiveDriverId } from "@/hooks/useEffectiveDriverId";
|
||||
import { getMembership } from '@/lib/leagueMembership';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import { Modal } from '@/components/shared/Modal';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { useState } from 'react';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Text } from '@/ui/Text';
|
||||
|
||||
interface JoinRequestItemProps {
|
||||
driverId: string;
|
||||
formattedRequestedAt: string;
|
||||
requestedAt: string;
|
||||
onApprove: () => void;
|
||||
onReject: () => void;
|
||||
isApproving?: boolean;
|
||||
@@ -13,7 +13,7 @@ interface JoinRequestItemProps {
|
||||
|
||||
export function JoinRequestItem({
|
||||
driverId,
|
||||
formattedRequestedAt,
|
||||
requestedAt,
|
||||
onApprove,
|
||||
onReject,
|
||||
isApproving,
|
||||
@@ -47,7 +47,7 @@ export function JoinRequestItem({
|
||||
<Stack flexGrow={1}>
|
||||
<Text color="text-white" weight="medium" block>{driverId}</Text>
|
||||
<Text size="sm" color="text-gray-400" block>
|
||||
Requested {formattedRequestedAt}
|
||||
Requested {requestedAt}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Box } from '@/ui/Box';
|
||||
import { Group } from '@/ui/Group';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { InfoFlyout } from '@/ui/InfoFlyout';
|
||||
import { InfoFlyout } from '@/components/shared/InfoFlyout';
|
||||
import { Stepper } from '@/ui/Stepper';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { IconButton } from '@/ui/IconButton';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useMemo, useState } from "react";
|
||||
import { usePenaltyTypesReference } from "@/hooks/usePenaltyTypesReference";
|
||||
import type { PenaltyValueKindDTO } from "@/lib/types/PenaltyTypesReferenceDTO";
|
||||
import { ProtestViewModel } from "../../lib/view-models/ProtestViewModel";
|
||||
import { Modal } from "@/ui/Modal";
|
||||
import { Modal } from "@/components/shared/Modal";
|
||||
import { Button } from "@/ui/Button";
|
||||
import { Card } from "@/ui/Card";
|
||||
import { Stack } from "@/ui/Stack";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { IconButton } from '@/ui/IconButton';
|
||||
import { Image } from '@/ui/Image';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import { Modal } from '@/components/shared/Modal';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { ChevronLeft, ChevronRight, Download } from 'lucide-react';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
|
||||
|
||||
import { motion, useReducedMotion } from 'framer-motion';
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
|
||||
interface MockupStackProps {
|
||||
children: ReactNode;
|
||||
@@ -28,9 +29,13 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
|
||||
// On mobile or before mount, render without animations
|
||||
if (!isMounted || isMobile) {
|
||||
return (
|
||||
<div className="relative w-full h-full scale-60 sm:scale-70 md:scale-85 lg:scale-95 max-w-[85vw] mx-auto my-4 sm:my-0" style={{ perspective: '1200px' }}>
|
||||
<div
|
||||
className="absolute rounded-none bg-panel-gray/80 border border-border-gray/50"
|
||||
<Box position="relative" fullWidth fullHeight maxWidth="85vw" marginX="auto" marginY={{ base: 4, sm: 0 }} style={{ perspective: '1200px', transform: 'scale(var(--mockup-scale, 1))' }}>
|
||||
<Surface
|
||||
variant="muted"
|
||||
position="absolute"
|
||||
rounded="none"
|
||||
border
|
||||
borderColor="var(--ui-color-border-low)"
|
||||
style={{
|
||||
rotate: `${rotation1}deg`,
|
||||
zIndex: 1,
|
||||
@@ -43,8 +48,12 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="absolute rounded-none bg-panel-gray/90 border border-border-gray/50"
|
||||
<Surface
|
||||
variant="muted"
|
||||
position="absolute"
|
||||
rounded="none"
|
||||
border
|
||||
borderColor="var(--ui-color-border-low)"
|
||||
style={{
|
||||
rotate: `${rotation2}deg`,
|
||||
zIndex: 2,
|
||||
@@ -57,24 +66,34 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="relative z-10 w-full h-full rounded-none overflow-hidden border border-border-gray/30"
|
||||
<Box
|
||||
position="relative"
|
||||
zIndex={10}
|
||||
fullWidth
|
||||
fullHeight
|
||||
rounded="none"
|
||||
overflow="hidden"
|
||||
border
|
||||
borderColor="var(--ui-color-border-low)"
|
||||
style={{
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.45)',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop: render with animations
|
||||
return (
|
||||
<div className="relative w-full h-full scale-60 sm:scale-70 md:scale-85 lg:scale-95 max-w-[85vw] mx-auto my-4 sm:my-0" style={{ perspective: '1200px' }}>
|
||||
<Box position="relative" fullWidth fullHeight maxWidth="85vw" marginX="auto" marginY={{ base: 4, sm: 0 }} style={{ perspective: '1200px', transform: 'scale(var(--mockup-scale, 1))' }}>
|
||||
<motion.div
|
||||
className="absolute rounded-none bg-panel-gray/80 border border-border-gray/50"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
borderRadius: '0',
|
||||
backgroundColor: 'rgba(20, 22, 25, 0.8)',
|
||||
border: '1px solid rgba(35, 39, 43, 0.5)',
|
||||
rotate: `${rotation1}deg`,
|
||||
zIndex: 1,
|
||||
top: '-8px',
|
||||
@@ -89,8 +108,11 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
className="absolute rounded-none bg-panel-gray/90 border border-border-gray/50"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
borderRadius: '0',
|
||||
backgroundColor: 'rgba(20, 22, 25, 0.9)',
|
||||
border: '1px solid rgba(35, 39, 43, 0.5)',
|
||||
rotate: `${rotation2}deg`,
|
||||
zIndex: 2,
|
||||
top: '-4px',
|
||||
@@ -105,8 +127,14 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
className="relative z-10 w-full h-full rounded-none overflow-hidden border border-border-gray/30"
|
||||
style={{
|
||||
position: 'relative',
|
||||
zIndex: 10,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '0',
|
||||
overflow: 'hidden',
|
||||
border: '1px solid rgba(35, 39, 43, 0.3)',
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.45)',
|
||||
}}
|
||||
whileHover={
|
||||
@@ -129,7 +157,12 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
|
||||
transition={{ duration: 0.4, delay: 0.2 }}
|
||||
>
|
||||
<motion.div
|
||||
className="absolute inset-0 pointer-events-none rounded-none"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
pointerEvents: 'none',
|
||||
borderRadius: '0',
|
||||
}}
|
||||
whileHover={
|
||||
shouldReduceMotion
|
||||
? {}
|
||||
@@ -141,6 +174,6 @@ export function MockupStack({ children, index = 0 }: MockupStackProps) {
|
||||
/>
|
||||
{children}
|
||||
</motion.div>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,18 +35,9 @@ export function ProtestWorkflowMockup() {
|
||||
},
|
||||
];
|
||||
|
||||
const getStatusStyles = (status: string) => {
|
||||
switch (status) {
|
||||
case 'pending': return 'bg-panel-gray border-gray-700 text-gray-600';
|
||||
case 'active': return 'bg-warning-amber/10 border-warning-amber text-warning-amber';
|
||||
case 'resolved': return 'bg-success-green/10 border-success-green text-success-green';
|
||||
default: return 'bg-panel-gray border-gray-700 text-gray-600';
|
||||
}
|
||||
};
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Box position="relative" fullWidth fullHeight bg="graphite-black" rounded="none" overflow="hidden" p={3} display="flex" flexDirection="col" justifyContent="center" gap={4}>
|
||||
<Box position="relative" fullWidth fullHeight bg="var(--ui-color-bg-base)" rounded="none" overflow="hidden" p={3} display="flex" flexDirection="col" justifyContent="center" gap={4}>
|
||||
<Box display="flex" alignItems="center" justifyContent="center" gap={3}>
|
||||
{steps.map((step, i) => (
|
||||
<Box key={step.name} display="flex" alignItems="center">
|
||||
@@ -61,17 +52,19 @@ export function ProtestWorkflowMockup() {
|
||||
mb={1}
|
||||
border
|
||||
borderWidth="1px"
|
||||
className={getStatusStyles(step.status)}
|
||||
bg={step.status === 'pending' ? 'var(--ui-color-bg-surface)' : step.status === 'active' ? 'rgba(255, 190, 77, 0.1)' : 'rgba(16, 185, 129, 0.1)'}
|
||||
borderColor={step.status === 'pending' ? 'var(--ui-color-border-default)' : step.status === 'active' ? 'var(--ui-color-intent-warning)' : 'var(--ui-color-intent-success)'}
|
||||
color={step.status === 'pending' ? 'var(--ui-color-text-low)' : step.status === 'active' ? 'var(--ui-color-intent-warning)' : 'var(--ui-color-intent-success)'}
|
||||
>
|
||||
<Box as="svg" w="5" h="5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={step.icon} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Text size="xs" color="text-white" weight="bold" textAlign="center" className="uppercase tracking-widest">{step.name}</Text>
|
||||
<Text size="xs" color="text-white" weight="bold" textAlign="center" uppercase letterSpacing="widest">{step.name}</Text>
|
||||
</Box>
|
||||
{i < steps.length - 1 && (
|
||||
<Box as="svg" w="4" h="4" mx={1} viewBox="0 0 24 24" fill="none">
|
||||
<path d="M5 12h14m-7-7l7 7-7 7" stroke="#198CFF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M5 12h14m-7-7l7 7-7 7" stroke="var(--ui-color-intent-primary)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
@@ -83,7 +76,7 @@ export function ProtestWorkflowMockup() {
|
||||
position="absolute"
|
||||
insetY="0"
|
||||
left="0"
|
||||
bg="primary-accent"
|
||||
bg="var(--ui-color-intent-primary)"
|
||||
style={{ width: `${((activeStep + 1) / steps.length) * 100}%` }}
|
||||
/>
|
||||
</Box>
|
||||
@@ -104,7 +97,7 @@ export function ProtestWorkflowMockup() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box position="relative" fullWidth fullHeight bg="graphite-black" rounded="none" overflow="hidden" p={{ base: 1.5, sm: 3, md: 5, lg: 8 }} display="flex" flexDirection="col" justifyContent="center" gap={{ base: 2, sm: 4, md: 6, lg: 8 }}>
|
||||
<Box position="relative" fullWidth fullHeight bg="var(--ui-color-bg-base)" rounded="none" overflow="hidden" p={{ base: 1.5, sm: 3, md: 5, lg: 8 }} display="flex" flexDirection="col" justifyContent="center" gap={{ base: 2, sm: 4, md: 6, lg: 8 }}>
|
||||
<Box display="flex" flexDirection={{ base: 'col', md: 'row' }} alignItems="center" justifyContent="center" gap={{ base: 2, sm: 3, md: 4 }}>
|
||||
{steps.map((step, i) => (
|
||||
<Box key={step.name} display="flex" alignItems="center" flexShrink={0}>
|
||||
@@ -131,10 +124,12 @@ export function ProtestWorkflowMockup() {
|
||||
mb={{ base: 1, sm: 1.5, md: 2 }}
|
||||
border
|
||||
borderWidth="1px"
|
||||
className={getStatusStyles(step.status)}
|
||||
bg={step.status === 'pending' ? 'var(--ui-color-bg-surface)' : step.status === 'active' ? 'rgba(255, 190, 77, 0.1)' : 'rgba(16, 185, 129, 0.1)'}
|
||||
borderColor={step.status === 'pending' ? 'var(--ui-color-border-default)' : step.status === 'active' ? 'var(--ui-color-intent-warning)' : 'var(--ui-color-intent-success)'}
|
||||
color={step.status === 'pending' ? 'var(--ui-color-text-low)' : step.status === 'active' ? 'var(--ui-color-intent-warning)' : 'var(--ui-color-intent-success)'}
|
||||
whileHover={shouldReduceMotion ? {} : {
|
||||
scale: 1.05,
|
||||
borderColor: '#198CFF',
|
||||
borderColor: 'var(--ui-color-intent-primary)',
|
||||
transition: { duration: 0.2 }
|
||||
}}
|
||||
>
|
||||
@@ -142,7 +137,7 @@ export function ProtestWorkflowMockup() {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={step.icon} />
|
||||
</Box>
|
||||
{step.status === 'active' && (
|
||||
<Box position="absolute" top="-1px" left="-1px" w="2" h="2" borderTop borderLeft borderColor="warning-amber" />
|
||||
<Box position="absolute" top="-1px" left="-1px" w="2" h="2" borderTop borderLeft borderColor="var(--ui-color-intent-warning)" />
|
||||
)}
|
||||
</Box>
|
||||
<Text
|
||||
@@ -151,7 +146,8 @@ export function ProtestWorkflowMockup() {
|
||||
color="text-white"
|
||||
weight="bold"
|
||||
textAlign="center"
|
||||
className="uppercase tracking-widest"
|
||||
uppercase
|
||||
letterSpacing="widest"
|
||||
>
|
||||
{step.name}
|
||||
</Text>
|
||||
@@ -159,13 +155,13 @@ export function ProtestWorkflowMockup() {
|
||||
|
||||
{i < steps.length - 1 && (
|
||||
<Box
|
||||
className="hidden md:block"
|
||||
display={{ base: 'none', md: 'block' }}
|
||||
position="relative"
|
||||
ml={2}
|
||||
mr={2}
|
||||
>
|
||||
<Box as="svg" w={{ base: 3, sm: 4, md: 5 }} h={{ base: 3, sm: 4, md: 5 }} viewBox="0 0 24 24" fill="none">
|
||||
<path d="M5 12h14m-7-7l7 7-7 7" stroke="#198CFF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" opacity={0.5} />
|
||||
<path d="M5 12h14m-7-7l7 7-7 7" stroke="var(--ui-color-intent-primary)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" opacity={0.5} />
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
@@ -189,7 +185,7 @@ export function ProtestWorkflowMockup() {
|
||||
position="absolute"
|
||||
insetY="0"
|
||||
left="0"
|
||||
bg="primary-accent"
|
||||
bg="var(--ui-color-intent-primary)"
|
||||
initial={{ width: '0%' }}
|
||||
animate={{ width: `${((activeStep + 1) / steps.length) * 100}%` }}
|
||||
transition={{ duration: 0.5, ease: 'easeOut' }}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import { Modal } from '@/components/shared/Modal';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Box } from '@/ui/Box';
|
||||
|
||||
interface OnboardingShellProps {
|
||||
children: React.ReactNode;
|
||||
@@ -16,40 +17,40 @@ interface OnboardingShellProps {
|
||||
*/
|
||||
export function OnboardingShell({ children, header, footer, sidebar }: OnboardingShellProps) {
|
||||
return (
|
||||
<Stack minHeight="screen" bg="bg-near-black" color="text-white">
|
||||
<Box minHeight="100vh" bg="rgba(10,10,10,1)" color="white">
|
||||
{header && (
|
||||
<Stack borderBottom borderColor="border-charcoal-outline" py={4} bg="bg-deep-charcoal">
|
||||
<Box borderBottom borderColor="rgba(255,255,255,0.1)" py={4} bg="rgba(20,22,25,1)">
|
||||
<Container size="md">
|
||||
{header}
|
||||
</Container>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Stack flex={1} py={12}>
|
||||
<Box flexGrow={1} py={12}>
|
||||
<Container size="md">
|
||||
<Stack direction="row" gap={12}>
|
||||
<Stack flex={1}>
|
||||
<Box flexGrow={1}>
|
||||
<Stack gap={8}>
|
||||
{children}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{sidebar && (
|
||||
<Stack w="80" display={{ base: 'none', lg: 'block' }}>
|
||||
<Box w="80" display={{ base: 'none', lg: 'block' }}>
|
||||
{sidebar}
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Container>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{footer && (
|
||||
<Stack borderTop borderColor="border-charcoal-outline" py={6} bg="bg-deep-charcoal">
|
||||
<Box borderTop borderColor="rgba(255,255,255,0.1)" py={6} bg="rgba(20,22,25,1)">
|
||||
<Container size="md">
|
||||
{footer}
|
||||
</Container>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Button } from '@/ui/Button';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { InfoBox } from '@/ui/InfoBox';
|
||||
import { Input } from '@/ui/Input';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import { Modal } from '@/components/shared/Modal';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Select } from '@/ui/Select';
|
||||
import { Text } from '@/ui/Text';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Input } from '@/ui/Input';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import { Modal } from '@/components/shared/Modal';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Select } from '@/ui/Select';
|
||||
import { Text } from '@/ui/Text';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { Box } from './Box';
|
||||
import { Icon } from './Icon';
|
||||
import { Surface } from './Surface';
|
||||
import { Text } from './Text';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Box } from '@/ui/Box';
|
||||
|
||||
export interface AccordionProps {
|
||||
title: string;
|
||||
@@ -36,15 +36,23 @@ export const Accordion = ({
|
||||
|
||||
return (
|
||||
<Surface variant="muted" rounded="lg" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
|
||||
<button
|
||||
<Box
|
||||
as="button"
|
||||
onClick={handleToggle}
|
||||
className="w-full flex items-center justify-between px-4 py-3 hover:bg-white/5 transition-colors"
|
||||
fullWidth
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="between"
|
||||
paddingX={4}
|
||||
paddingY={3}
|
||||
hoverBg="rgba(255,255,255,0.05)"
|
||||
transition
|
||||
>
|
||||
<Text weight="bold" size="sm" variant="high">
|
||||
{title}
|
||||
</Text>
|
||||
<Icon icon={isOpen ? ChevronUp : ChevronDown} size={4} intent="low" />
|
||||
</button>
|
||||
</Box>
|
||||
|
||||
{isOpen && (
|
||||
<Box padding={4} borderTop>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import { Modal } from '@/components/shared/Modal';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
@@ -1,6 +1,3 @@
|
||||
/* eslint-disable gridpilot-rules/no-raw-html-in-app */
|
||||
|
||||
|
||||
import { Check, ChevronDown, Globe, Search } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { CountryFlag } from '@/ui/CountryFlag';
|
||||
@@ -10,6 +7,7 @@ import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Input } from '@/ui/Input';
|
||||
|
||||
export interface Country {
|
||||
code: string;
|
||||
@@ -115,32 +113,31 @@ export function CountrySelect({
|
||||
return (
|
||||
<Box ref={containerRef} position="relative">
|
||||
{/* Trigger Button */}
|
||||
<button
|
||||
<Box
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => !disabled && setIsOpen(!isOpen)}
|
||||
disabled={disabled}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
borderRadius: 'var(--ui-radius-md)',
|
||||
border: 'none',
|
||||
padding: '0.75rem 1rem',
|
||||
backgroundColor: 'var(--ui-color-bg-surface-muted)',
|
||||
color: 'white',
|
||||
boxShadow: 'var(--ui-shadow-sm)',
|
||||
transition: 'all 150ms',
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
outline: 'none',
|
||||
ring: '1px inset',
|
||||
borderColor: error ? 'var(--ui-color-intent-critical)' : 'var(--ui-color-border-default)'
|
||||
} as any}
|
||||
className={error ? 'ring-warning-amber' : 'ring-charcoal-outline focus:ring-primary-blue'}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="between"
|
||||
fullWidth
|
||||
rounded="md"
|
||||
paddingX={4}
|
||||
paddingY={3}
|
||||
bg="var(--ui-color-bg-surface-muted)"
|
||||
color="white"
|
||||
shadow="sm"
|
||||
transition="all 150ms"
|
||||
cursor={disabled ? 'not-allowed' : 'pointer'}
|
||||
opacity={disabled ? 0.5 : 1}
|
||||
outline="none"
|
||||
border
|
||||
borderColor={error ? 'var(--ui-color-intent-critical)' : 'var(--ui-color-border-default)'}
|
||||
hoverBorderColor={!disabled ? 'var(--ui-color-intent-primary)' : undefined}
|
||||
>
|
||||
<Group gap={3}>
|
||||
<Globe className="w-4 h-4 text-gray-500" />
|
||||
<Icon icon={Globe} size={4} intent="low" />
|
||||
{selectedCountry ? (
|
||||
<Group gap={2}>
|
||||
<CountryFlag countryCode={selectedCountry.code} size="md" />
|
||||
@@ -150,8 +147,16 @@ export function CountrySelect({
|
||||
<Text variant="low">{placeholder}</Text>
|
||||
)}
|
||||
</Group>
|
||||
<ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
<Icon
|
||||
icon={ChevronDown}
|
||||
size={4}
|
||||
intent="low"
|
||||
style={{
|
||||
transition: 'transform 200ms',
|
||||
transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Dropdown */}
|
||||
{isOpen && (
|
||||
@@ -167,60 +172,49 @@ export function CountrySelect({
|
||||
overflow="hidden"
|
||||
>
|
||||
{/* Search Input */}
|
||||
<Box padding={2} borderBottom="1px solid var(--ui-color-border-muted)">
|
||||
<Box position="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Search countries..."
|
||||
style={{
|
||||
width: '100%',
|
||||
borderRadius: 'var(--ui-radius-md)',
|
||||
border: 'none',
|
||||
padding: '0.5rem 1rem 0.5rem 2.25rem',
|
||||
backgroundColor: 'var(--ui-color-bg-base)',
|
||||
color: 'white',
|
||||
fontSize: '0.875rem',
|
||||
outline: 'none'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box padding={2} borderBottom>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Search countries..."
|
||||
fullWidth
|
||||
size="sm"
|
||||
icon={<Icon icon={Search} size={4} intent="low" />}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Country List */}
|
||||
<Box overflowY="auto" maxHeight="15rem">
|
||||
{filteredCountries.length > 0 ? (
|
||||
filteredCountries.map((country) => (
|
||||
<button
|
||||
<Box
|
||||
as="button"
|
||||
key={country.code}
|
||||
type="button"
|
||||
onClick={() => handleSelect(country.code)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
padding: '0.625rem 1rem',
|
||||
textAlign: 'left',
|
||||
fontSize: '0.875rem',
|
||||
transition: 'colors 150ms',
|
||||
border: 'none',
|
||||
backgroundColor: value === country.code ? 'rgba(25, 140, 255, 0.2)' : 'transparent',
|
||||
color: value === country.code ? 'white' : 'var(--ui-color-text-med)',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="between"
|
||||
fullWidth
|
||||
paddingX={4}
|
||||
paddingY={2.5}
|
||||
textAlign="left"
|
||||
transition="colors 150ms"
|
||||
border="none"
|
||||
bg={value === country.code ? 'rgba(25, 140, 255, 0.2)' : 'transparent'}
|
||||
color={value === country.code ? 'white' : 'var(--ui-color-text-med)'}
|
||||
cursor="pointer"
|
||||
hoverBg="rgba(255, 255, 255, 0.05)"
|
||||
>
|
||||
<Group gap={3}>
|
||||
<CountryFlag countryCode={country.code} size="md" />
|
||||
<Text as="span">{country.name}</Text>
|
||||
</Group>
|
||||
{value === country.code && (
|
||||
<Check className="w-4 h-4 text-primary-blue" />
|
||||
<Icon icon={Check} size={4} intent="primary" />
|
||||
)}
|
||||
</button>
|
||||
</Box>
|
||||
))
|
||||
) : (
|
||||
<Box paddingX={4} paddingY={6} textAlign="center">
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { HelpCircle, X } from 'lucide-react';
|
||||
import React, { ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Box } from './Box';
|
||||
import { Heading } from './Heading';
|
||||
import { Icon } from './Icon';
|
||||
import { IconButton } from './IconButton';
|
||||
import { Surface } from './Surface';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { IconButton } from '@/ui/IconButton';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
|
||||
export interface InfoFlyoutProps {
|
||||
isOpen: boolean;
|
||||
@@ -1,12 +1,12 @@
|
||||
import { X } from 'lucide-react';
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Box } from './Box';
|
||||
import { Button } from './Button';
|
||||
import { Heading } from './Heading';
|
||||
import { IconButton } from './IconButton';
|
||||
import { Surface } from './Surface';
|
||||
import { Text } from './Text';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { IconButton } from '@/ui/IconButton';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Text } from '@/ui/Text';
|
||||
|
||||
export interface ModalProps {
|
||||
children: ReactNode;
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import React, { createContext, useContext, ReactNode } from 'react';
|
||||
import { Theme } from './Theme';
|
||||
import { defaultTheme } from './themes/default';
|
||||
import { Theme } from '@/ui/theme/Theme';
|
||||
import { defaultTheme } from '@/ui/theme/themes/default';
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme;
|
||||
@@ -3,7 +3,7 @@ import { Text } from '@/ui/Text';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { ConfirmDialog } from '@/ui/ConfirmDialog';
|
||||
import { ConfirmDialog } from '@/components/shared/ConfirmDialog';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Card } from '@/ui/Card';
|
||||
@@ -17,7 +17,7 @@ import { Skeleton } from '@/ui/Skeleton';
|
||||
import { LoadingSpinner } from '@/ui/LoadingSpinner';
|
||||
import { Badge } from '@/ui/Badge';
|
||||
|
||||
import { ProgressLine } from '@/ui/ProgressLine';
|
||||
import { ProgressLine } from '@/components/shared/ProgressLine';
|
||||
import { SharedEmptyState } from './SharedEmptyState';
|
||||
|
||||
export {
|
||||
|
||||
@@ -107,7 +107,8 @@ export function UploadDropzone({
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<input
|
||||
<Box
|
||||
as="input"
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileSelect}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EmptyState } from '@/ui/EmptyState';
|
||||
import { ErrorDisplay } from '@/ui/ErrorDisplay';
|
||||
import { ErrorDisplay } from '@/components/shared/ErrorDisplay';
|
||||
import { LoadingWrapper } from '@/ui/LoadingWrapper';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
import { Inbox, List, LucideIcon } from 'lucide-react';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { EmptyState } from '@/ui/EmptyState';
|
||||
import { ErrorDisplay } from '@/ui/ErrorDisplay';
|
||||
import { ErrorDisplay } from '@/components/shared/ErrorDisplay';
|
||||
import { LoadingWrapper } from '@/ui/LoadingWrapper';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
Car,
|
||||
TrendingUp,
|
||||
} from 'lucide-react';
|
||||
import { WorkflowMockup, WorkflowStep } from '@/ui/WorkflowMockup';
|
||||
import { WorkflowMockup, WorkflowStep } from '@/components/mockups/WorkflowMockup';
|
||||
|
||||
const WORKFLOW_STEPS: WorkflowStep[] = [
|
||||
{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Group } from '@/ui/Group';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { StatusDot } from '@/ui/StatusDot';
|
||||
|
||||
interface TeamsDirectoryProps {
|
||||
children: ReactNode;
|
||||
@@ -14,32 +14,33 @@ interface TeamsDirectoryProps {
|
||||
|
||||
export function TeamsDirectory({ children, title, subtitle }: TeamsDirectoryProps) {
|
||||
return (
|
||||
<Box as="main" bg="base-black" minHeight="screen">
|
||||
<Container size="lg">
|
||||
<Box paddingY={12}>
|
||||
<Stack gap={10}>
|
||||
{title && (
|
||||
<Stack direction="row" align="center" gap={2} mb={6}>
|
||||
<Box w="2" h="2" bg="primary-accent" />
|
||||
<Text size="xs" weight="bold" color="text-gray-400" uppercase>{title}</Text>
|
||||
</Stack>
|
||||
)}
|
||||
{children}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
<Container size="lg" py={12}>
|
||||
<Group direction="column" gap={10} fullWidth>
|
||||
{title && (
|
||||
<Group direction="row" align="center" gap={2}>
|
||||
<StatusDot intent="primary" size="md" />
|
||||
<Text size="xs" weight="bold" variant="low" uppercase>{title}</Text>
|
||||
</Group>
|
||||
)}
|
||||
{children}
|
||||
</Group>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function TeamsDirectorySection({ children, title, accentColor = "primary-accent" }: { children: ReactNode, title: string, accentColor?: string }) {
|
||||
const intentMap: Record<string, 'primary' | 'success' | 'warning' | 'critical' | 'telemetry'> = {
|
||||
'primary-accent': 'primary',
|
||||
'telemetry-aqua': 'telemetry',
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack direction="row" align="center" gap={2} mb={6}>
|
||||
<Box w="2" h="2" bg={accentColor} />
|
||||
<Text size="xs" weight="bold" color="text-gray-400" uppercase>{title}</Text>
|
||||
</Stack>
|
||||
<Group direction="column" gap={6} fullWidth>
|
||||
<Group direction="row" align="center" gap={2}>
|
||||
<StatusDot intent={intentMap[accentColor] || 'primary'} size="md" />
|
||||
<Text size="xs" weight="bold" variant="low" uppercase>{title}</Text>
|
||||
</Group>
|
||||
{children}
|
||||
</Box>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { LeagueRoleDisplay } from './LeagueRoleDisplay';
|
||||
|
||||
describe('LeagueRoleDisplay', () => {
|
||||
it('should be defined', () => {
|
||||
expect(LeagueRoleDisplay).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { LeagueWizardValidationMessages } from './LeagueWizardValidationMessages';
|
||||
|
||||
describe('LeagueWizardValidationMessages', () => {
|
||||
it('should be defined', () => {
|
||||
expect(LeagueWizardValidationMessages).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -6,21 +6,18 @@ import { AdminSectionHeader } from '@/components/admin/AdminSectionHeader';
|
||||
import { AdminStatsPanel } from '@/components/admin/AdminStatsPanel';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { AdminDashboardViewData } from '@/lib/view-data/AdminDashboardViewData';
|
||||
import {
|
||||
SharedBox,
|
||||
SharedButton,
|
||||
SharedCard,
|
||||
SharedContainer,
|
||||
SharedIcon,
|
||||
SharedGrid,
|
||||
SharedStack,
|
||||
SharedText,
|
||||
SharedBadge
|
||||
} from '@/components/shared/UIComponents';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Badge } from '@/ui/Badge';
|
||||
import { QuickActionLink } from '@/ui/QuickActionLink';
|
||||
import {
|
||||
Activity,
|
||||
ArrowRight,
|
||||
Clock,
|
||||
RefreshCw,
|
||||
Shield,
|
||||
@@ -70,93 +67,95 @@ export function AdminDashboardTemplate({
|
||||
];
|
||||
|
||||
return (
|
||||
<SharedContainer size="lg">
|
||||
<SharedBox paddingY={8}>
|
||||
<SharedStack gap={8}>
|
||||
<Container size="lg">
|
||||
<Box paddingY={8}>
|
||||
<Stack gap={8}>
|
||||
<AdminHeaderPanel
|
||||
title="Admin Dashboard"
|
||||
description="System-wide telemetry and operations control"
|
||||
isLoading={isLoading}
|
||||
actions={
|
||||
<SharedButton
|
||||
<Button
|
||||
onClick={onRefresh}
|
||||
disabled={isLoading}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon={<SharedIcon icon={RefreshCw} size={3} animate={isLoading ? 'spin' : 'none'} />}
|
||||
>
|
||||
Refresh Telemetry
|
||||
</SharedButton>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={RefreshCw} size={3} animate={isLoading ? 'spin' : 'none'} />
|
||||
<Text>Refresh Telemetry</Text>
|
||||
</Stack>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<AdminStatsPanel stats={stats} />
|
||||
|
||||
<SharedGrid cols={{ base: 1, md: 2 }} gap={6}>
|
||||
<Grid responsiveGridCols={{ base: 1, md: 2 }} gap={6}>
|
||||
{/* System Health & Status */}
|
||||
<SharedCard p={6}>
|
||||
<SharedStack gap={6}>
|
||||
<Card padding={6}>
|
||||
<Stack gap={6}>
|
||||
<AdminSectionHeader
|
||||
title="System Status"
|
||||
actions={
|
||||
<SharedBadge variant="success">
|
||||
<SharedStack direction="row" align="center" gap={1.5}>
|
||||
<SharedIcon icon={Activity} size={3} />
|
||||
<SharedText>Operational</SharedText>
|
||||
</SharedStack>
|
||||
</SharedBadge>
|
||||
<Badge variant="success">
|
||||
<Stack direction="row" align="center" gap={1.5}>
|
||||
<Icon icon={Activity} size={3} />
|
||||
<Text>Operational</Text>
|
||||
</Stack>
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
|
||||
<SharedStack gap={4}>
|
||||
<SharedBox borderTop borderColor="border-gray" opacity={0.3} />
|
||||
<SharedBox pt={0}>
|
||||
<SharedStack direction="row" align="center" justify="between" py={2}>
|
||||
<SharedText size="sm" color="text-gray-400">Suspended Users</SharedText>
|
||||
<SharedText weight="bold" color="text-warning-amber">{viewData.stats.suspendedUsers}</SharedText>
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
<SharedBox borderTop borderColor="border-gray" opacity={0.3} />
|
||||
<SharedBox>
|
||||
<SharedStack direction="row" align="center" justify="between" py={2}>
|
||||
<SharedText size="sm" color="text-gray-400">Deleted Users</SharedText>
|
||||
<SharedText weight="bold" color="text-error-red">{viewData.stats.deletedUsers}</SharedText>
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
<SharedBox borderTop borderColor="border-gray" opacity={0.3} />
|
||||
<SharedBox>
|
||||
<SharedStack direction="row" align="center" justify="between" py={2}>
|
||||
<SharedText size="sm" color="text-gray-400">New Registrations (24h)</SharedText>
|
||||
<SharedText weight="bold" color="text-primary-blue">{viewData.stats.newUsersToday}</SharedText>
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
</SharedStack>
|
||||
</SharedStack>
|
||||
</SharedCard>
|
||||
<Stack gap={4}>
|
||||
<Box borderTop borderColor="rgba(255,255,255,0.1)" />
|
||||
<Box pt={0}>
|
||||
<Stack direction="row" align="center" justify="between" py={2}>
|
||||
<Text size="sm" color="text-gray-400">Suspended Users</Text>
|
||||
<Text weight="bold" color="warning-amber">{viewData.stats.suspendedUsers}</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box borderTop borderColor="rgba(255,255,255,0.1)" />
|
||||
<Box>
|
||||
<Stack direction="row" align="center" justify="between" py={2}>
|
||||
<Text size="sm" color="text-gray-400">Deleted Users</Text>
|
||||
<Text weight="bold" color="critical-red">{viewData.stats.deletedUsers}</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box borderTop borderColor="rgba(255,255,255,0.1)" />
|
||||
<Box>
|
||||
<Stack direction="row" align="center" justify="between" py={2}>
|
||||
<Text size="sm" color="text-gray-400">New Registrations (24h)</Text>
|
||||
<Text weight="bold" color="primary-accent">{viewData.stats.newUsersToday}</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* Quick Operations */}
|
||||
<SharedCard p={6}>
|
||||
<SharedStack gap={6}>
|
||||
<Card padding={6}>
|
||||
<Stack gap={6}>
|
||||
<AdminSectionHeader title="Quick Operations" />
|
||||
<SharedGrid cols={1} gap={3}>
|
||||
<Grid responsiveGridCols={{ base: 1 }} gap={3}>
|
||||
<QuickActionLink href={routes.admin.users} label="User Management" icon={Users} />
|
||||
<QuickActionLink href="/admin" label="Security & Roles" icon={Shield} />
|
||||
<QuickActionLink href="/admin" label="System Audit Logs" icon={Activity} />
|
||||
</SharedGrid>
|
||||
</SharedStack>
|
||||
</SharedCard>
|
||||
</SharedGrid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<AdminDangerZonePanel
|
||||
title="System Maintenance"
|
||||
description="Perform destructive system-wide operations. Use with extreme caution."
|
||||
>
|
||||
<SharedButton variant="danger" size="sm">
|
||||
<Button variant="danger" size="sm">
|
||||
Enter Maintenance Mode
|
||||
</SharedButton>
|
||||
</Button>
|
||||
</AdminDangerZonePanel>
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
</SharedContainer>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import { FormEvent, ReactNode } from 'react';
|
||||
import { FormEvent } from 'react';
|
||||
import { LeagueReviewSummary } from '@/components/leagues/LeagueReviewSummary';
|
||||
import {
|
||||
SharedBox,
|
||||
SharedButton,
|
||||
SharedStack,
|
||||
SharedText,
|
||||
SharedIcon,
|
||||
SharedContainer
|
||||
} from '@/components/shared/UIComponents';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Input } from '@/ui/Input';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import {
|
||||
AlertCircle,
|
||||
Award,
|
||||
Calendar,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
FileText,
|
||||
Loader2,
|
||||
Scale,
|
||||
Sparkles,
|
||||
Trophy,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
import { LeagueBasicsSection } from '@/components/leagues/LeagueBasicsSection';
|
||||
import { LeagueDropSection } from '@/components/leagues/LeagueDropSection';
|
||||
@@ -90,45 +82,45 @@ export function CreateLeagueWizardTemplate({
|
||||
const CurrentStepIcon = currentStepData?.icon ?? FileText;
|
||||
|
||||
return (
|
||||
<SharedBox as="form" onSubmit={onSubmit} maxWidth="4xl" mx="auto" pb={8}>
|
||||
<Box as="form" onSubmit={onSubmit} maxWidth="4xl" mx="auto" pb={8}>
|
||||
{/* Header with icon */}
|
||||
<SharedBox mb={8}>
|
||||
<SharedStack direction="row" align="center" gap={3} mb={3}>
|
||||
<SharedBox display="flex" h="11" w="11" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue/20" border borderColor="border-primary-blue/20">
|
||||
<SharedIcon icon={Sparkles} size={5} color="text-primary-blue" />
|
||||
</SharedBox>
|
||||
<SharedBox>
|
||||
<Heading level={1} fontSize={{ base: '2xl', sm: '3xl' }}>
|
||||
<Box mb={8}>
|
||||
<Stack direction="row" align="center" gap={3} mb={3}>
|
||||
<Box display="flex" h="11" w="11" alignItems="center" justifyContent="center" rounded="xl" bg="primary-accent" opacity={0.2} border borderColor="primary-accent">
|
||||
<Icon icon={Sparkles} size={5} color="primary-accent" />
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading level={1}>
|
||||
Create a new league
|
||||
</Heading>
|
||||
<SharedText size="sm" color="text-gray-500" block>
|
||||
<Text size="sm" color="text-gray-500" block>
|
||||
We'll also set up your first season in {steps.length} easy steps.
|
||||
</SharedText>
|
||||
<SharedText size="xs" color="text-gray-500" block mt={1}>
|
||||
</Text>
|
||||
<Text size="xs" color="text-gray-500" block mt={1}>
|
||||
A league is your long-term brand. Each season is a block of races you can run again and again.
|
||||
</SharedText>
|
||||
</SharedBox>
|
||||
</SharedStack>
|
||||
</SharedBox>
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Desktop Progress Bar */}
|
||||
<SharedBox display={{ base: 'none', md: 'block' }} mb={8}>
|
||||
<SharedBox position="relative">
|
||||
<Box display={{ base: 'none', md: 'block' }} mb={8}>
|
||||
<Box position="relative">
|
||||
{/* Background track */}
|
||||
<SharedBox position="absolute" top="5" left="6" right="6" h="0.5" bg="bg-charcoal-outline" rounded="full" />
|
||||
<Box position="absolute" top="5" left="6" right="6" h="0.5" bg="rgba(255,255,255,0.1)" rounded="full" />
|
||||
{/* Progress fill */}
|
||||
<SharedBox
|
||||
<Box
|
||||
position="absolute"
|
||||
top="5"
|
||||
left="6"
|
||||
h="0.5"
|
||||
bg="bg-gradient-to-r from-primary-blue to-neon-aqua"
|
||||
bg="primary-accent"
|
||||
rounded="full"
|
||||
transition
|
||||
width={`calc(${((step - 1) / (steps.length - 1)) * 100}% - 48px)`}
|
||||
width={`${((step - 1) / (steps.length - 1)) * 100}%`}
|
||||
/>
|
||||
|
||||
<SharedBox position="relative" display="flex" justifyContent="between">
|
||||
<Box position="relative" display="flex" justifyContent="between">
|
||||
{steps.map((wizardStep) => {
|
||||
const isCompleted = wizardStep.id < step;
|
||||
const isCurrent = wizardStep.id === step;
|
||||
@@ -136,7 +128,7 @@ export function CreateLeagueWizardTemplate({
|
||||
const StepIcon = wizardStep.icon;
|
||||
|
||||
return (
|
||||
<SharedBox
|
||||
<Box
|
||||
as="button"
|
||||
key={wizardStep.id}
|
||||
type="button"
|
||||
@@ -145,12 +137,11 @@ export function CreateLeagueWizardTemplate({
|
||||
display="flex"
|
||||
flexDirection="col"
|
||||
alignItems="center"
|
||||
bg="bg-transparent"
|
||||
borderStyle="none"
|
||||
bg="transparent"
|
||||
cursor={isAccessible ? 'pointer' : 'not-allowed'}
|
||||
opacity={!isAccessible ? 0.6 : 1}
|
||||
>
|
||||
<SharedBox
|
||||
<Box
|
||||
position="relative"
|
||||
zIndex={10}
|
||||
display="flex"
|
||||
@@ -160,129 +151,83 @@ export function CreateLeagueWizardTemplate({
|
||||
justifyContent="center"
|
||||
rounded="full"
|
||||
transition
|
||||
bg={isCurrent || isCompleted ? 'bg-primary-blue' : 'bg-iron-gray'}
|
||||
color={isCurrent || isCompleted ? 'text-white' : 'text-gray-400'}
|
||||
border={!isCurrent && !isCompleted}
|
||||
borderColor="border-charcoal-outline"
|
||||
shadow={isCurrent ? '0_0_24px_rgba(25,140,255,0.5)' : undefined}
|
||||
transform={isCurrent ? 'scale-110' : isCompleted ? 'hover:scale-105' : undefined}
|
||||
bg={isCurrent || isCompleted ? 'primary-accent' : 'rgba(255,255,255,0.1)'}
|
||||
color={isCurrent || isCompleted ? 'white' : 'text-gray-400'}
|
||||
>
|
||||
{isCompleted ? (
|
||||
<SharedIcon icon={Check} size={4} strokeWidth={3} />
|
||||
<Icon icon={Check} size={4} />
|
||||
) : (
|
||||
<SharedIcon icon={StepIcon} size={4} />
|
||||
<Icon icon={StepIcon} size={4} />
|
||||
)}
|
||||
</SharedBox>
|
||||
<SharedBox mt={2} textAlign="center">
|
||||
<SharedText
|
||||
</Box>
|
||||
<Box mt={2} textAlign="center">
|
||||
<Text
|
||||
size="xs"
|
||||
weight="medium"
|
||||
transition
|
||||
color={isCurrent ? 'text-white' : isCompleted ? 'text-primary-blue' : isAccessible ? 'text-gray-400' : 'text-gray-500'}
|
||||
color={isCurrent ? 'white' : isCompleted ? 'primary-accent' : isAccessible ? 'text-gray-400' : 'text-gray-500'}
|
||||
>
|
||||
{wizardStep.label}
|
||||
</SharedText>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
|
||||
{/* Mobile Progress */}
|
||||
<SharedBox display={{ base: 'block', md: 'none' }} mb={6}>
|
||||
<SharedBox display="flex" alignItems="center" justifyContent="between" mb={2}>
|
||||
<SharedStack direction="row" align="center" gap={2}>
|
||||
<SharedIcon icon={CurrentStepIcon} size={4} color="text-primary-blue" />
|
||||
<SharedText size="sm" weight="medium" color="text-white">{currentStepData?.label}</SharedText>
|
||||
</SharedStack>
|
||||
<SharedText size="xs" color="text-gray-500">
|
||||
{step}/{steps.length}
|
||||
</SharedText>
|
||||
</SharedBox>
|
||||
<SharedBox h="1.5" bg="bg-charcoal-outline" rounded="full" overflow="hidden">
|
||||
<SharedBox
|
||||
h="full"
|
||||
bg="bg-gradient-to-r from-primary-blue to-neon-aqua"
|
||||
rounded="full"
|
||||
transition
|
||||
height="full"
|
||||
width={`${(step / steps.length) * 100}%`}
|
||||
/>
|
||||
</SharedBox>
|
||||
{/* Step dots */}
|
||||
<SharedBox display="flex" justifyContent="between" mt={2} px={0.5}>
|
||||
{steps.map((s) => (
|
||||
<SharedBox
|
||||
key={s.id}
|
||||
h="1.5"
|
||||
rounded="full"
|
||||
transition
|
||||
width={s.id === step ? '4' : '1.5'}
|
||||
bg={s.id === step ? 'bg-primary-blue' : s.id < step ? 'bg-primary-blue/60' : 'bg-charcoal-outline'}
|
||||
/>
|
||||
))}
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Main Card */}
|
||||
<Card position="relative" overflow="hidden">
|
||||
{/* Top gradient accent */}
|
||||
<SharedBox position="absolute" top="0" left="0" right="0" h="1" bg="bg-gradient-to-r from-transparent via-primary-blue to-transparent" />
|
||||
|
||||
{/* Step header */}
|
||||
<SharedBox display="flex" alignItems="start" gap={4} mb={6}>
|
||||
<SharedBox display="flex" h="12" w="12" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue/10" flexShrink={0} transition>
|
||||
<SharedIcon icon={CurrentStepIcon} size={6} color="text-primary-blue" />
|
||||
</SharedBox>
|
||||
<SharedBox flexGrow={1} minWidth="0">
|
||||
<Heading level={2} fontSize={{ base: 'xl', md: '2xl' }} color="text-white">
|
||||
<SharedStack direction="row" align="center" gap={2} flexWrap="wrap">
|
||||
<SharedText>{getStepTitle(step)}</SharedText>
|
||||
<SharedText size="xs" weight="medium" px={2} py={0.5} rounded="full" border borderColor="border-charcoal-outline" bg="bg-iron-gray/60" color="text-gray-300">
|
||||
{getStepContextLabel(step)}
|
||||
</SharedText>
|
||||
</SharedStack>
|
||||
<Box display="flex" alignItems="start" gap={4} mb={6}>
|
||||
<Box display="flex" h="12" w="12" alignItems="center" justifyContent="center" rounded="xl" bg="rgba(25,140,255,0.1)" flexShrink={0} transition>
|
||||
<Icon icon={CurrentStepIcon} size={6} color="primary-accent" />
|
||||
</Box>
|
||||
<Box flexGrow={1} minWidth="0">
|
||||
<Heading level={2} color="white">
|
||||
<Stack direction="row" align="center" gap={2} flexWrap="wrap">
|
||||
<Text>{getStepTitle(step)}</Text>
|
||||
<Box px={2} py={0.5} rounded="full" border borderColor="rgba(255,255,255,0.1)" bg="rgba(255,255,255,0.05)">
|
||||
<Text size="xs" weight="medium" color="text-gray-300">
|
||||
{getStepContextLabel(step)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Heading>
|
||||
<SharedText size="sm" color="text-gray-400" block mt={1}>
|
||||
<Text size="sm" color="text-gray-400" block mt={1}>
|
||||
{getStepSubtitle(step)}
|
||||
</SharedText>
|
||||
</SharedBox>
|
||||
<SharedBox display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={1.5} px={3} py={1.5} rounded="full" bg="bg-deep-graphite" border borderColor="border-charcoal-outline">
|
||||
<SharedText size="xs" color="text-gray-500">Step</SharedText>
|
||||
<SharedText size="sm" weight="semibold" color="text-white">{step}</SharedText>
|
||||
<SharedText size="xs" color="text-gray-500">/ {steps.length}</SharedText>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</Text>
|
||||
</Box>
|
||||
<Box display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={1.5} px={3} py={1.5} rounded="full" bg="rgba(0,0,0,0.2)" border borderColor="rgba(255,255,255,0.1)">
|
||||
<Text size="xs" color="text-gray-500">Step</Text>
|
||||
<Text size="sm" weight="semibold" color="white">{step}</Text>
|
||||
<Text size="xs" color="text-gray-500">/ {steps.length}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Divider */}
|
||||
<SharedBox h="px" bg="bg-gradient-to-r from-transparent via-charcoal-outline to-transparent" mb={6} />
|
||||
|
||||
{/* Step content with min-height for consistency */}
|
||||
<SharedBox minHeight="320px">
|
||||
{/* Step content */}
|
||||
<Box minHeight="320px">
|
||||
{step === 1 && (
|
||||
<SharedBox animate="fade-in" gap={8} display="flex" flexDirection="col">
|
||||
<Stack gap={8}>
|
||||
<LeagueBasicsSection
|
||||
form={form}
|
||||
onChange={onFormChange}
|
||||
errors={errors.basics ?? {}}
|
||||
/>
|
||||
<SharedBox rounded="xl" border borderColor="border-charcoal-outline" bg="bg-iron-gray/40" p={4}>
|
||||
<SharedBox display="flex" alignItems="center" justifyContent="between" gap={2} mb={2}>
|
||||
<SharedBox>
|
||||
<SharedText size="xs" weight="semibold" color="text-gray-300" uppercase letterSpacing="wide">
|
||||
First season
|
||||
</SharedText>
|
||||
<SharedText size="xs" color="text-gray-500" block>
|
||||
Name the first season that will run in this league.
|
||||
</SharedText>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
<SharedBox mt={2} display="flex" flexDirection="col" gap={2}>
|
||||
<SharedText as="label" size="sm" weight="medium" color="text-gray-300" block>
|
||||
<Box rounded="xl" border borderColor="rgba(255,255,255,0.1)" bg="rgba(255,255,255,0.05)" p={4}>
|
||||
<Box mb={2}>
|
||||
<Text size="xs" weight="semibold" color="text-gray-300" uppercase letterSpacing="wide">
|
||||
First season
|
||||
</Text>
|
||||
<Text size="xs" color="text-gray-500" block>
|
||||
Name the first season that will run in this league.
|
||||
</Text>
|
||||
</Box>
|
||||
<Stack gap={2}>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-300" block>
|
||||
Season name
|
||||
</SharedText>
|
||||
</Text>
|
||||
<Input
|
||||
value={form.seasonName ?? ''}
|
||||
onChange={(e) =>
|
||||
@@ -293,75 +238,63 @@ export function CreateLeagueWizardTemplate({
|
||||
}
|
||||
placeholder="e.g., Season 1 (2025)"
|
||||
/>
|
||||
<SharedText size="xs" color="text-gray-500" block>
|
||||
<Text size="xs" color="text-gray-500" block>
|
||||
Seasons are the individual competitive runs inside your league. You can run Season 2, Season 3, or parallel seasons later.
|
||||
</SharedText>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<SharedBox animate="fade-in">
|
||||
<LeagueVisibilitySection
|
||||
form={form}
|
||||
onChange={onFormChange}
|
||||
errors={
|
||||
errors.basics?.visibility
|
||||
? { visibility: errors.basics.visibility }
|
||||
: {}
|
||||
}
|
||||
/>
|
||||
</SharedBox>
|
||||
<LeagueVisibilitySection
|
||||
form={form}
|
||||
onChange={onFormChange}
|
||||
errors={
|
||||
errors.basics?.visibility
|
||||
? { visibility: errors.basics.visibility }
|
||||
: {}
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<SharedBox animate="fade-in" display="flex" flexDirection="col" gap={4}>
|
||||
<SharedBox mb={2}>
|
||||
<SharedText size="xs" color="text-gray-500" block>
|
||||
<Stack gap={4}>
|
||||
<Box>
|
||||
<Text size="xs" color="text-gray-500" block>
|
||||
Applies to: First season of this league.
|
||||
</SharedText>
|
||||
<SharedText size="xs" color="text-gray-500" block>
|
||||
These settings only affect this season. Future seasons can use different formats.
|
||||
</SharedText>
|
||||
</SharedBox>
|
||||
</Text>
|
||||
</Box>
|
||||
<LeagueStructureSection
|
||||
form={form}
|
||||
onChange={onFormChange}
|
||||
readOnly={false}
|
||||
/>
|
||||
</SharedBox>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{step === 4 && (
|
||||
<SharedBox animate="fade-in" display="flex" flexDirection="col" gap={4}>
|
||||
<SharedBox mb={2}>
|
||||
<SharedText size="xs" color="text-gray-500" block>
|
||||
<Stack gap={4}>
|
||||
<Box>
|
||||
<Text size="xs" color="text-gray-500" block>
|
||||
Applies to: First season of this league.
|
||||
</SharedText>
|
||||
<SharedText size="xs" color="text-gray-500" block>
|
||||
These settings only affect this season. Future seasons can use different formats.
|
||||
</SharedText>
|
||||
</SharedBox>
|
||||
</Text>
|
||||
</Box>
|
||||
<LeagueTimingsSection
|
||||
form={form}
|
||||
onChange={onFormChange}
|
||||
errors={errors.timings ?? {}}
|
||||
/>
|
||||
</SharedBox>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{step === 5 && (
|
||||
<SharedBox animate="fade-in" display="flex" flexDirection="col" gap={8}>
|
||||
<SharedBox mb={2}>
|
||||
<SharedText size="xs" color="text-gray-500" block>
|
||||
<Stack gap={8}>
|
||||
<Box>
|
||||
<Text size="xs" color="text-gray-500" block>
|
||||
Applies to: First season of this league.
|
||||
</SharedText>
|
||||
<SharedText size="xs" color="text-gray-500" block>
|
||||
These settings only affect this season. Future seasons can use different formats.
|
||||
</SharedText>
|
||||
</SharedBox>
|
||||
{/* Scoring Pattern Selection */}
|
||||
</Text>
|
||||
</Box>
|
||||
<ScoringPatternSection
|
||||
scoring={form.scoring || {}}
|
||||
presets={presets}
|
||||
@@ -370,116 +303,96 @@ export function CreateLeagueWizardTemplate({
|
||||
onChangePatternId={onScoringPresetChange}
|
||||
onToggleCustomScoring={onToggleCustomScoring}
|
||||
/>
|
||||
|
||||
{/* Divider */}
|
||||
<SharedBox h="px" bg="bg-gradient-to-r from-transparent via-charcoal-outline to-transparent" />
|
||||
|
||||
{/* Championships & Drop Rules side by side on larger screens */}
|
||||
<SharedBox display="grid" gridCols={{ base: 1, lg: 2 }} gap={6}>
|
||||
<Grid responsiveGridCols={{ base: 1, lg: 2 }} gap={6}>
|
||||
<ChampionshipsSection form={form} onChange={onFormChange} readOnly={presetsLoading} />
|
||||
<LeagueDropSection form={form} onChange={onFormChange} readOnly={false} />
|
||||
</SharedBox>
|
||||
</Grid>
|
||||
|
||||
{errors.submit && (
|
||||
<SharedBox display="flex" alignItems="start" gap={3} rounded="lg" bg="bg-warning-amber/10" p={4} border borderColor="border-warning-amber/20">
|
||||
<SharedIcon icon={AlertCircle} size={5} color="text-warning-amber" flexShrink={0} mt={0.5} />
|
||||
<SharedText size="sm" color="text-warning-amber">{errors.submit}</SharedText>
|
||||
</SharedBox>
|
||||
<Box display="flex" alignItems="start" gap={3} rounded="lg" bg="rgba(245,158,11,0.1)" p={4} border borderColor="rgba(245,158,11,0.2)">
|
||||
<Icon icon={AlertCircle} size={5} color="warning-amber" />
|
||||
<Text size="sm" color="warning-amber">{errors.submit}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</SharedBox>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{step === 6 && (
|
||||
<SharedBox animate="fade-in" display="flex" flexDirection="col" gap={4}>
|
||||
<SharedBox mb={2}>
|
||||
<SharedText size="xs" color="text-gray-500" block>
|
||||
<Stack gap={4}>
|
||||
<Box>
|
||||
<Text size="xs" color="text-gray-500" block>
|
||||
Applies to: First season of this league.
|
||||
</SharedText>
|
||||
<SharedText size="xs" color="text-gray-500" block>
|
||||
These settings only affect this season. Future seasons can use different formats.
|
||||
</SharedText>
|
||||
</SharedBox>
|
||||
</Text>
|
||||
</Box>
|
||||
<LeagueStewardingSection
|
||||
form={form}
|
||||
onChange={onFormChange}
|
||||
readOnly={false}
|
||||
/>
|
||||
</SharedBox>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{step === 7 && (
|
||||
<SharedBox animate="fade-in" display="flex" flexDirection="col" gap={6}>
|
||||
<Stack gap={6}>
|
||||
<LeagueReviewSummary form={form} presets={presets} />
|
||||
{errors.submit && (
|
||||
<SharedBox display="flex" alignItems="start" gap={3} rounded="lg" bg="bg-warning-amber/10" p={4} border borderColor="border-warning-amber/20">
|
||||
<SharedIcon icon={AlertCircle} size={5} color="text-warning-amber" flexShrink={0} mt={0.5} />
|
||||
<SharedText size="sm" color="text-warning-amber">{errors.submit}</SharedText>
|
||||
</SharedBox>
|
||||
<Box display="flex" alignItems="start" gap={3} rounded="lg" bg="rgba(245,158,11,0.1)" p={4} border borderColor="rgba(245,158,11,0.2)">
|
||||
<Icon icon={AlertCircle} size={5} color="warning-amber" />
|
||||
<Text size="sm" color="warning-amber">{errors.submit}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</SharedBox>
|
||||
</Stack>
|
||||
)}
|
||||
</SharedBox>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
{/* Navigation */}
|
||||
<SharedBox display="flex" alignItems="center" justifyContent="between" mt={6}>
|
||||
<SharedButton
|
||||
<Box display="flex" alignItems="center" justifyContent="between" mt={6}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={step === 1 || loading}
|
||||
onClick={onPreviousStep}
|
||||
icon={<SharedIcon icon={ChevronLeft} size={4} />}
|
||||
>
|
||||
<SharedText display={{ base: 'none', md: 'inline-block' }}>Back</SharedText>
|
||||
</SharedButton>
|
||||
|
||||
<SharedBox display="flex" alignItems="center" gap={3}>
|
||||
{/* Mobile step dots */}
|
||||
<SharedBox display={{ base: 'flex', sm: 'none' }} alignItems="center" gap={1}>
|
||||
{steps.map((s) => (
|
||||
<SharedBox
|
||||
key={s.id}
|
||||
h="1.5"
|
||||
rounded="full"
|
||||
transition
|
||||
width={s.id === step ? '3' : '1.5'}
|
||||
bg={s.id === step ? 'bg-primary-blue' : s.id < step ? 'bg-primary-blue/50' : 'bg-charcoal-outline'}
|
||||
/>
|
||||
))}
|
||||
</SharedBox>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={ChevronLeft} size={4} />
|
||||
<Text display={{ base: 'none', md: 'inline-block' }}>Back</Text>
|
||||
</Stack>
|
||||
</Button>
|
||||
|
||||
<Box display="flex" alignItems="center" gap={3}>
|
||||
{step < 7 ? (
|
||||
<SharedButton
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
disabled={loading}
|
||||
onClick={onNextStep}
|
||||
icon={<SharedIcon icon={ChevronRight} size={4} />}
|
||||
>
|
||||
<SharedText>Continue</SharedText>
|
||||
</SharedButton>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Text>Continue</Text>
|
||||
<Icon icon={ChevronRight} size={4} />
|
||||
</Stack>
|
||||
</Button>
|
||||
) : (
|
||||
<SharedButton
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={loading}
|
||||
style={{ minWidth: '150px' }}
|
||||
icon={loading ? <SharedIcon icon={Loader2} size={4} animate="spin" /> : <SharedIcon icon={Sparkles} size={4} />}
|
||||
>
|
||||
{loading ? (
|
||||
<SharedText>Creating…</SharedText>
|
||||
) : (
|
||||
<SharedText>Create League</SharedText>
|
||||
)}
|
||||
</SharedButton>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
{loading ? <Icon icon={Loader2} size={4} animate="spin" /> : <Icon icon={Sparkles} size={4} />}
|
||||
<Text>{loading ? 'Creating…' : 'Create League'}</Text>
|
||||
</Stack>
|
||||
</Button>
|
||||
)}
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Helper text */}
|
||||
<SharedText size="xs" color="text-gray-500" align="center" block mt={4}>
|
||||
<Text size="xs" color="text-gray-500" align="center" block mt={4}>
|
||||
This will create your league and its first season. You can edit both later.
|
||||
</SharedText>
|
||||
</SharedBox>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { DashboardControlBar } from '@/components/dashboard/DashboardControlBar';
|
||||
import { DashboardKpiRow } from '@/components/dashboard/DashboardKpiRow';
|
||||
import { DashboardRail } from '@/components/dashboard/DashboardRail';
|
||||
import { DashboardShell } from '@/components/dashboard/DashboardShell';
|
||||
import { RecentActivityTable, type ActivityItem } from '@/components/dashboard/RecentActivityTable';
|
||||
import { TelemetryPanel } from '@/components/dashboard/TelemetryPanel';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import type { DashboardViewData } from '@/lib/view-data/DashboardViewData';
|
||||
import { Avatar } from '@/ui/Avatar';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { IconButton } from '@/ui/IconButton';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Bell, Calendar, LayoutDashboard, Search, Settings, Trophy, Users } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
interface DashboardTemplateProps {
|
||||
viewData: DashboardViewData;
|
||||
onNavigateToRaces: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,8 +22,10 @@ interface DashboardTemplateProps {
|
||||
* Composes semantic dashboard components into a high-density data environment.
|
||||
* Complies with architectural constraints by using UI primitives.
|
||||
*/
|
||||
export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
|
||||
const router = useRouter();
|
||||
export function DashboardTemplate({
|
||||
viewData,
|
||||
onNavigateToRaces,
|
||||
}: DashboardTemplateProps) {
|
||||
const {
|
||||
currentDriver,
|
||||
nextRace,
|
||||
@@ -43,11 +38,11 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
|
||||
} = viewData;
|
||||
|
||||
const kpiItems = [
|
||||
{ label: 'Rating', value: currentDriver.rating, color: 'var(--color-telemetry)' },
|
||||
{ label: 'Rank', value: `#${currentDriver.rank}`, color: 'var(--color-warning)' },
|
||||
{ label: 'Rating', value: currentDriver.rating, intent: 'primary' as const },
|
||||
{ label: 'Rank', value: `#${currentDriver.rank}`, intent: 'warning' as const },
|
||||
{ label: 'Starts', value: currentDriver.totalRaces },
|
||||
{ label: 'Wins', value: currentDriver.wins, color: 'var(--color-success)' },
|
||||
{ label: 'Podiums', value: currentDriver.podiums, color: 'var(--color-warning)' },
|
||||
{ label: 'Wins', value: currentDriver.wins, intent: 'success' as const },
|
||||
{ label: 'Podiums', value: currentDriver.podiums, intent: 'warning' as const },
|
||||
{ label: 'Leagues', value: activeLeaguesCount },
|
||||
];
|
||||
|
||||
@@ -59,68 +54,8 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
|
||||
status: item.type === 'race_result' ? 'success' : 'info'
|
||||
}));
|
||||
|
||||
const railContent = (
|
||||
<DashboardRail>
|
||||
<Stack direction="col" align="center" gap={6} fullWidth>
|
||||
<Box h="8" w="8" rounded="sm" bg="primary-accent" display="flex" alignItems="center" justifyContent="center">
|
||||
<Text size="xs" weight="bold">GP</Text>
|
||||
</Box>
|
||||
<IconButton
|
||||
icon={LayoutDashboard}
|
||||
onClick={() => router.push(routes.protected.dashboard)}
|
||||
variant="ghost"
|
||||
color="primary-accent"
|
||||
/>
|
||||
<IconButton
|
||||
icon={Trophy}
|
||||
onClick={() => router.push(routes.public.leagues)}
|
||||
variant="ghost"
|
||||
color="var(--color-text-low)"
|
||||
/>
|
||||
<IconButton
|
||||
icon={Calendar}
|
||||
onClick={() => router.push(routes.public.races)}
|
||||
variant="ghost"
|
||||
color="var(--color-text-low)"
|
||||
/>
|
||||
<IconButton
|
||||
icon={Users}
|
||||
onClick={() => router.push(routes.public.teams)}
|
||||
variant="ghost"
|
||||
color="var(--color-text-low)"
|
||||
/>
|
||||
</Stack>
|
||||
<Box mt="auto" display="flex" flexDirection="col" alignItems="center" gap={6} pb={4}>
|
||||
<IconButton
|
||||
icon={Settings}
|
||||
onClick={() => router.push(routes.protected.profile)}
|
||||
variant="ghost"
|
||||
color="var(--color-text-low)"
|
||||
/>
|
||||
</Box>
|
||||
</DashboardRail>
|
||||
);
|
||||
|
||||
const controlBarActions = (
|
||||
<Stack direction="row" align="center" gap={4}>
|
||||
<IconButton icon={Search} variant="ghost" color="var(--color-text-low)" />
|
||||
<Box position="relative">
|
||||
<IconButton icon={Bell} variant="ghost" color="var(--color-text-low)" />
|
||||
<Box position="absolute" top="0" right="0" h="1.5" w="1.5" rounded="full" bg="critical-red" />
|
||||
</Box>
|
||||
<Avatar
|
||||
src={currentDriver.avatarUrl}
|
||||
alt={currentDriver.name}
|
||||
size={32}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
return (
|
||||
<DashboardShell
|
||||
rail={railContent}
|
||||
controlBar={<DashboardControlBar title="Telemetry Workspace" actions={controlBarActions} />}
|
||||
>
|
||||
<Stack gap={6}>
|
||||
{/* KPI Overview */}
|
||||
<DashboardKpiRow items={kpiItems} />
|
||||
|
||||
@@ -132,14 +67,14 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
|
||||
<TelemetryPanel title="Active Session">
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
<Box>
|
||||
<Text size="xs" color="var(--color-text-low)" mb={1} block>Next Event</Text>
|
||||
<Text size="xs" color="text-gray-500" mb={1} block>Next Event</Text>
|
||||
<Text size="lg" weight="bold" block>{nextRace.track}</Text>
|
||||
<Text size="xs" color="var(--color-telemetry)" font="mono" block>{nextRace.car}</Text>
|
||||
<Text size="xs" color="primary-accent" font="mono" block>{nextRace.car}</Text>
|
||||
</Box>
|
||||
<Box textAlign="right">
|
||||
<Text size="xs" color="var(--color-text-low)" mb={1} block>Starts In</Text>
|
||||
<Text size="xl" font="mono" weight="bold" color="var(--color-warning)" block>{nextRace.timeUntil}</Text>
|
||||
<Text size="xs" color="var(--color-text-low)" block>{nextRace.formattedDate} @ {nextRace.formattedTime}</Text>
|
||||
<Text size="xs" color="text-gray-500" mb={1} block>Starts In</Text>
|
||||
<Text size="xl" font="mono" weight="bold" color="warning-amber" block>{nextRace.timeUntil}</Text>
|
||||
<Text size="xs" color="text-gray-500" block>{nextRace.formattedDate} @ {nextRace.formattedTime}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</TelemetryPanel>
|
||||
@@ -150,7 +85,7 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
|
||||
<RecentActivityTable items={activityItems} />
|
||||
) : (
|
||||
<Box py={8} textAlign="center">
|
||||
<Text italic color="var(--color-text-low)">No recent activity recorded.</Text>
|
||||
<Text italic color="text-gray-500">No recent activity recorded.</Text>
|
||||
</Box>
|
||||
)}
|
||||
</TelemetryPanel>
|
||||
@@ -164,18 +99,18 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
|
||||
{hasLeagueStandings ? (
|
||||
<Stack direction="col" gap={3}>
|
||||
{leagueStandings.map((standing) => (
|
||||
<Box key={standing.leagueId} display="flex" alignItems="center" justifyContent="between" borderBottom borderColor="rgba(35, 39, 43, 0.3)" pb={2}>
|
||||
<Box key={standing.leagueId} display="flex" alignItems="center" justifyContent="between" borderBottom borderColor="rgba(255, 255, 255, 0.1)" pb={2}>
|
||||
<Box>
|
||||
<Text size="xs" weight="bold" truncate block maxWidth="180px">{standing.leagueName}</Text>
|
||||
<Text size="xs" color="var(--color-text-low)" block>Pos: {standing.position} / {standing.totalDrivers}</Text>
|
||||
<Text size="xs" color="text-gray-500" block>Pos: {standing.position} / {standing.totalDrivers}</Text>
|
||||
</Box>
|
||||
<Text size="sm" font="mono" weight="bold" color="var(--color-telemetry)">{standing.points} PTS</Text>
|
||||
<Text size="sm" font="mono" weight="bold" color="primary-accent">{standing.points} PTS</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<Box py={4} textAlign="center">
|
||||
<Text italic color="var(--color-text-low)">No active championships.</Text>
|
||||
<Text italic color="text-gray-500">No active championships.</Text>
|
||||
</Box>
|
||||
)}
|
||||
</TelemetryPanel>
|
||||
@@ -183,21 +118,21 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
|
||||
<TelemetryPanel title="Upcoming Schedule">
|
||||
<Stack direction="col" gap={4}>
|
||||
{upcomingRaces.slice(0, 3).map((race) => (
|
||||
<Box key={race.id} group cursor="pointer">
|
||||
<Box key={race.id} cursor="pointer">
|
||||
<Box display="flex" justifyContent="between" alignItems="start" mb={1}>
|
||||
<Text size="xs" weight="bold" groupHoverTextColor="var(--color-primary)" transition>{race.track}</Text>
|
||||
<Text size="xs" font="mono" color="var(--color-text-low)">{race.timeUntil}</Text>
|
||||
<Text size="xs" weight="bold">{race.track}</Text>
|
||||
<Text size="xs" font="mono" color="text-gray-500">{race.timeUntil}</Text>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="xs" color="var(--color-text-low)">{race.car}</Text>
|
||||
<Text size="xs" color="var(--color-text-low)">{race.formattedDate}</Text>
|
||||
<Text size="xs" color="text-gray-500">{race.car}</Text>
|
||||
<Text size="xs" color="text-gray-500">{race.formattedDate}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
<Button
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
onClick={() => router.push(routes.public.races)}
|
||||
onClick={onNavigateToRaces}
|
||||
>
|
||||
<Text size="xs" weight="bold" uppercase letterSpacing="widest">View Full Schedule</Text>
|
||||
</Button>
|
||||
@@ -206,6 +141,6 @@ export function DashboardTemplate({ viewData }: DashboardTemplateProps) {
|
||||
</Stack>
|
||||
</Box>
|
||||
</Grid>
|
||||
</DashboardShell>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ import { DriverTableRow } from '@/components/drivers/DriverTableRow';
|
||||
import { EmptyState } from '@/ui/EmptyState';
|
||||
import type { DriversViewData } from '@/lib/types/view-data/DriversViewData';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Group } from '@/ui/Group';
|
||||
import { Search } from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
interface DriversTemplateProps {
|
||||
viewData: DriversViewData | null;
|
||||
@@ -27,14 +28,9 @@ export function DriversTemplate({
|
||||
onDriverClick,
|
||||
onViewLeaderboard
|
||||
}: DriversTemplateProps) {
|
||||
const drivers = viewData?.drivers || [];
|
||||
const totalRaces = viewData?.totalRaces || 0;
|
||||
const totalWins = viewData?.totalWins || 0;
|
||||
const activeCount = viewData?.activeCount || 0;
|
||||
|
||||
return (
|
||||
<Container size="lg" py={8}>
|
||||
<Stack gap={10}>
|
||||
<Group direction="column" gap={10} fullWidth>
|
||||
<DriversDirectoryHeader
|
||||
totalDriversLabel={viewData?.totalDriversLabel || '0'}
|
||||
activeDriversLabel={viewData?.activeCountLabel || '0'}
|
||||
@@ -73,7 +69,7 @@ export function DriversTemplate({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,12 +15,8 @@ import { CompanionAutomationMockup } from '@/components/mockups/CompanionAutomat
|
||||
import { RaceHistoryMockup } from '@/components/mockups/RaceHistoryMockup';
|
||||
import { SimPlatformMockup } from '@/components/mockups/SimPlatformMockup';
|
||||
import { ModeGuard } from '@/components/shared/ModeGuard';
|
||||
import { DiscoverySection } from '@/ui/DiscoverySection';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { Section } from '@/ui/Section';
|
||||
import { Text } from '@/ui/Text';
|
||||
|
||||
export interface HomeViewData {
|
||||
isAlpha: boolean;
|
||||
@@ -53,7 +49,7 @@ interface HomeTemplateProps {
|
||||
*/
|
||||
export function HomeTemplate({ viewData }: HomeTemplateProps) {
|
||||
return (
|
||||
<Box color="text-white">
|
||||
<Box as="main">
|
||||
{/* Hero Section */}
|
||||
<HomeHeader
|
||||
title="Modern Motorsport Infrastructure."
|
||||
@@ -146,29 +142,15 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
|
||||
|
||||
{/* Discovery Grid */}
|
||||
<ModeGuard feature="alpha_discovery">
|
||||
<Section py={24} variant="dark">
|
||||
<Container>
|
||||
<Box maxWidth="2xl" mb={16}>
|
||||
<Box display="flex" alignItems="center" borderLeft borderStyle="solid" borderWidth="2px" borderColor="primary-accent" px={4} mb={4}>
|
||||
<Text size="xs" weight="bold" uppercase letterSpacing="widest" color="text-primary-accent">
|
||||
Live Ecosystem
|
||||
</Text>
|
||||
</Box>
|
||||
<Heading level={2} fontSize={{ base: '3xl', md: '4xl' }} weight="bold" letterSpacing="tight" color="text-white" mb={6}>
|
||||
DISCOVER THE GRID
|
||||
</Heading>
|
||||
<Text size="lg" color="text-gray-400" leading="relaxed">
|
||||
Explore leagues, teams, and races that make up the GridPilot ecosystem.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Grid cols={1} lgCols={3} gap={8}>
|
||||
<LeagueSummaryPanel leagues={viewData.topLeagues} />
|
||||
<TeamSummaryPanel teams={viewData.teams} />
|
||||
<RecentRacesPanel races={viewData.upcomingRaces} />
|
||||
</Grid>
|
||||
</Container>
|
||||
</Section>
|
||||
<DiscoverySection
|
||||
title="DISCOVER THE GRID"
|
||||
subtitle="Live Ecosystem"
|
||||
description="Explore leagues, teams, and races that make up the GridPilot ecosystem."
|
||||
>
|
||||
<LeagueSummaryPanel leagues={viewData.topLeagues} />
|
||||
<TeamSummaryPanel teams={viewData.teams} />
|
||||
<RecentRacesPanel races={viewData.upcomingRaces} />
|
||||
</DiscoverySection>
|
||||
</ModeGuard>
|
||||
|
||||
{/* CTA & FAQ */}
|
||||
|
||||
@@ -95,7 +95,7 @@ export function LeagueAdminScheduleTemplate({
|
||||
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Text size="sm" color="text-gray-300">
|
||||
Status: <Text weight="medium" color="text-white">{publishedLabel}</Text>
|
||||
Status: <Text weight="medium" color="white">{publishedLabel}</Text>
|
||||
</Text>
|
||||
<Button
|
||||
onClick={onPublishToggle}
|
||||
@@ -107,12 +107,12 @@ export function LeagueAdminScheduleTemplate({
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<Box pt={6} borderTop="1px solid" borderColor="border-neutral-800">
|
||||
<Box pt={6} borderTop borderColor="rgba(255,255,255,0.1)">
|
||||
<Box mb={4}>
|
||||
<Heading level={2}>{isEditing ? 'Edit race' : 'Add race'}</Heading>
|
||||
</Box>
|
||||
|
||||
<Grid cols={3} gap={4}>
|
||||
<Grid responsiveGridCols={{ base: 1, md: 3 }} gap={4}>
|
||||
<Box>
|
||||
<Text size="sm" color="text-gray-300" block mb={2}>Track</Text>
|
||||
<Input
|
||||
@@ -161,7 +161,7 @@ export function LeagueAdminScheduleTemplate({
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box pt={6} borderTop="1px solid" borderColor="border-neutral-800">
|
||||
<Box pt={6} borderTop borderColor="rgba(255,255,255,0.1)">
|
||||
<Box mb={4}>
|
||||
<Heading level={2}>Races</Heading>
|
||||
</Box>
|
||||
@@ -178,7 +178,7 @@ export function LeagueAdminScheduleTemplate({
|
||||
>
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Box>
|
||||
<Text weight="medium" color="text-white" block>{race.name}</Text>
|
||||
<Text weight="medium" color="white" block>{race.name}</Text>
|
||||
<Text size="xs" color="text-gray-400" block mt={1}>{race.scheduledAt}</Text>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -23,19 +23,19 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
|
||||
<Surface variant="dark" border rounded="lg" padding={6}>
|
||||
<Stack gap={6}>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Box p={2} bg="bg-primary-blue/10" rounded="md" border borderColor="border-primary-blue/20">
|
||||
<Icon icon={Settings} size={5} color="text-primary-blue" />
|
||||
<Box p={2} bg="rgba(25,140,255,0.1)" rounded="md" border borderColor="rgba(25,140,255,0.2)">
|
||||
<Icon icon={Settings} size={5} color="primary-accent" />
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading level={5} color="text-primary-blue">LEAGUE INFORMATION</Heading>
|
||||
<Heading level={5} color="primary-accent">LEAGUE INFORMATION</Heading>
|
||||
<Text size="xs" color="text-gray-500">Basic league details and identification</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Grid cols={1} mdCols={2} gap={6}>
|
||||
<Grid responsiveGridCols={{ base: 1, md: 2 }} gap={6}>
|
||||
<InfoItem label="Name" value={viewData.league.name} />
|
||||
<InfoItem label="Visibility" value={viewData.league.visibility} capitalize />
|
||||
<GridItem colSpan={2}>
|
||||
<GridItem responsiveColSpan={{ base: 1, md: 2 }}>
|
||||
<InfoItem label="Description" value={viewData.league.description} />
|
||||
</GridItem>
|
||||
<InfoItem label="Created" value={new Date(viewData.league.createdAt).toLocaleDateString()} />
|
||||
@@ -48,16 +48,16 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
|
||||
<Surface variant="dark" border rounded="lg" padding={6}>
|
||||
<Stack gap={6}>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Box p={2} bg="bg-performance-green/10" rounded="md" border borderColor="border-performance-green/20">
|
||||
<Icon icon={Trophy} size={5} color="text-performance-green" />
|
||||
<Box p={2} bg="rgba(16,185,129,0.1)" rounded="md" border borderColor="rgba(16,185,129,0.2)">
|
||||
<Icon icon={Trophy} size={5} color="text-success-green" />
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading level={5} color="text-performance-green">CONFIGURATION</Heading>
|
||||
<Heading level={5} color="text-success-green">CONFIGURATION</Heading>
|
||||
<Text size="xs" color="text-gray-500">League rules and participation limits</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Grid cols={1} mdCols={2} gap={6}>
|
||||
<Grid responsiveGridCols={{ base: 1, md: 2 }} gap={6}>
|
||||
<ConfigItem icon={Users} label="Max Drivers" value={viewData.config.maxDrivers} />
|
||||
<ConfigItem icon={Shield} label="Require Approval" value={viewData.config.requireApproval ? 'Yes' : 'No'} />
|
||||
<ConfigItem icon={Clock} label="Allow Late Join" value={viewData.config.allowLateJoin ? 'Yes' : 'No'} />
|
||||
@@ -69,12 +69,12 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
|
||||
{/* Note about forms */}
|
||||
<Surface variant="dark" border rounded="lg" padding={8}>
|
||||
<Stack align="center" gap={4}>
|
||||
<Box p={4} bg="bg-warning-amber/10" rounded="full" border borderColor="border-warning-amber/20">
|
||||
<Icon icon={Settings} size={8} color="text-warning-amber" />
|
||||
<Box p={4} bg="rgba(245,158,11,0.1)" rounded="full" border borderColor="rgba(245,158,11,0.2)">
|
||||
<Icon icon={Settings} size={8} color="warning-amber" />
|
||||
</Box>
|
||||
<Box textAlign="center">
|
||||
<Heading level={3}>Settings Management</Heading>
|
||||
<Text size="sm" color="text-gray-400" mt={2} display="block">
|
||||
<Text size="sm" color="text-gray-400" mt={2} block>
|
||||
Form-based editing and ownership transfer functionality will be implemented in future updates.
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -88,8 +88,8 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
|
||||
function InfoItem({ label, value, capitalize }: { label: string, value: string, capitalize?: boolean }) {
|
||||
return (
|
||||
<Box>
|
||||
<Text size="xs" weight="bold" color="text-gray-500" display="block" mb={1} letterSpacing="wider">{label.toUpperCase()}</Text>
|
||||
<Text color="text-white" weight="medium">{capitalize ? value.toUpperCase() : value}</Text>
|
||||
<Text size="xs" weight="bold" color="text-gray-500" block mb={1} letterSpacing="wider">{label.toUpperCase()}</Text>
|
||||
<Text color="white" weight="medium">{capitalize ? value.toUpperCase() : value}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -97,12 +97,12 @@ function InfoItem({ label, value, capitalize }: { label: string, value: string,
|
||||
function ConfigItem({ icon, label, value }: { icon: LucideIcon, label: string, value: string | number }) {
|
||||
return (
|
||||
<Stack direction="row" align="center" gap={4}>
|
||||
<Box center w={10} h={10} rounded="lg" bg="bg-white/5">
|
||||
<Box display="flex" alignItems="center" justifyContent="center" w={10} h={10} rounded="lg" bg="rgba(255,255,255,0.05)">
|
||||
<Icon icon={icon} size={5} color="text-gray-400" />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text size="xs" weight="bold" color="text-gray-500" display="block" letterSpacing="wider">{label.toUpperCase()}</Text>
|
||||
<Text color="text-white" weight="medium">{value}</Text>
|
||||
<Text size="xs" weight="bold" color="text-gray-500" block letterSpacing="wider">{label.toUpperCase()}</Text>
|
||||
<Text color="white" weight="medium">{value}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
|
||||
import { WalletSummaryPanel } from '@/components/leagues/WalletSummaryPanel';
|
||||
import type { LeagueWalletViewData } from '@/lib/view-data/leagues/LeagueWalletViewData';
|
||||
import {
|
||||
SharedBox,
|
||||
SharedButton,
|
||||
SharedStack,
|
||||
SharedText,
|
||||
SharedIcon,
|
||||
SharedContainer
|
||||
} from '@/components/shared/UIComponents';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Download } from 'lucide-react';
|
||||
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
|
||||
|
||||
@@ -21,23 +19,23 @@ interface LeagueWalletTemplateProps extends TemplateProps<LeagueWalletViewData>
|
||||
transactions: any[];
|
||||
}
|
||||
|
||||
export function LeagueWalletTemplate({ viewData, onExport, transactions }: LeagueWalletTemplateProps) {
|
||||
export function LeagueWalletTemplate({ viewData, onExport }: LeagueWalletTemplateProps) {
|
||||
return (
|
||||
<SharedContainer size="lg">
|
||||
<SharedBox paddingY={8}>
|
||||
<Container size="lg">
|
||||
<Box paddingY={8}>
|
||||
{/* Header */}
|
||||
<SharedBox display="flex" alignItems="center" justifyContent="between" mb={8}>
|
||||
<SharedBox>
|
||||
<Box display="flex" alignItems="center" justifyContent="between" mb={8}>
|
||||
<Box>
|
||||
<Heading level={1}>League Wallet</Heading>
|
||||
<SharedText color="text-gray-400">Manage your league's finances and payouts</SharedText>
|
||||
</SharedBox>
|
||||
<SharedButton variant="secondary" onClick={onExport}>
|
||||
<SharedStack direction="row" align="center" gap={2}>
|
||||
<SharedIcon icon={Download} size={4} />
|
||||
<SharedText>Export</SharedText>
|
||||
</SharedStack>
|
||||
</SharedButton>
|
||||
</SharedBox>
|
||||
<Text color="text-gray-400">Manage your league's finances and payouts</Text>
|
||||
</Box>
|
||||
<Button variant="secondary" onClick={onExport}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={Download} size={4} />
|
||||
<Text>Export</Text>
|
||||
</Stack>
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<WalletSummaryPanel
|
||||
formattedBalance={viewData.formattedBalance}
|
||||
@@ -48,13 +46,13 @@ export function LeagueWalletTemplate({ viewData, onExport, transactions }: Leagu
|
||||
/>
|
||||
|
||||
{/* Alpha Notice */}
|
||||
<SharedBox mt={6} rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/30" p={4}>
|
||||
<SharedText size="xs" color="text-gray-400">
|
||||
<SharedText weight="bold" color="text-warning-amber">Alpha Note:</SharedText> Wallet management is demonstration-only.
|
||||
<Box mt={6} rounded="lg" bg="rgba(245,158,11,0.1)" border borderColor="rgba(245,158,11,0.3)" p={4}>
|
||||
<Text size="xs" color="text-gray-400">
|
||||
<Text weight="bold" color="warning-amber">Alpha Note:</Text> Wallet management is demonstration-only.
|
||||
Real payment processing and bank integrations will be available when the payment system is fully implemented.
|
||||
</SharedText>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</SharedContainer>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,29 +3,20 @@
|
||||
import { LeagueCard } from '@/components/leagues/LeagueCardWrapper';
|
||||
import type { LeaguesViewData } from '@/lib/view-data/LeaguesViewData';
|
||||
import { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
|
||||
import {
|
||||
SharedBox,
|
||||
SharedButton,
|
||||
SharedStack,
|
||||
SharedText,
|
||||
SharedIcon,
|
||||
SharedContainer
|
||||
} from '@/components/shared/UIComponents';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Input } from '@/ui/Input';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Group } from '@/ui/Group';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Section } from '@/ui/Section';
|
||||
import { StatusDot } from '@/ui/StatusDot';
|
||||
import {
|
||||
Award,
|
||||
Clock,
|
||||
Flag,
|
||||
Flame,
|
||||
Globe,
|
||||
Plus,
|
||||
Search,
|
||||
Sparkles,
|
||||
Target,
|
||||
Timer,
|
||||
Trophy,
|
||||
Users,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import React from 'react';
|
||||
@@ -77,44 +68,42 @@ export function LeaguesTemplate({
|
||||
onClearFilters,
|
||||
}: LeaguesTemplateProps) {
|
||||
return (
|
||||
<SharedBox minHeight="screen" bg="zinc-950" color="text-zinc-200">
|
||||
<SharedBox maxWidth="7xl" mx="auto" px={{ base: 4, sm: 6, lg: 8 }} py={12}>
|
||||
<Container size="xl" py={12}>
|
||||
<Group direction="column" gap={16} fullWidth>
|
||||
{/* Hero */}
|
||||
<SharedBox as="header" display="flex" flexDirection={{ base: 'col', md: 'row' }} alignItems={{ base: 'start', md: 'end' }} justifyContent="between" gap={8} mb={16}>
|
||||
<SharedStack gap={4}>
|
||||
<SharedBox display="flex" alignItems="center" gap={3} color="text-blue-500">
|
||||
<Trophy size={24} />
|
||||
<SharedText fontSize="xs" weight="bold" uppercase letterSpacing="widest">Competition Hub</SharedText>
|
||||
</SharedBox>
|
||||
<Heading level={1} fontSize="5xl" weight="bold" color="text-white">
|
||||
Find Your <SharedText as="span" color="text-blue-500">Grid</SharedText>
|
||||
<Group direction={{ base: 'column', md: 'row' } as any} align={{ base: 'start', md: 'end' } as any} justify="between" gap={8} fullWidth>
|
||||
<Group direction="column" gap={4}>
|
||||
<Group direction="row" align="center" gap={3}>
|
||||
<Icon icon={Trophy} size={6} intent="primary" />
|
||||
<Text size="xs" weight="bold" uppercase letterSpacing="widest" color="text-primary-accent">Competition Hub</Text>
|
||||
</Group>
|
||||
<Heading level={1} size="5xl" weight="bold">
|
||||
Find Your <Text as="span" color="text-primary-accent">Grid</Text>
|
||||
</Heading>
|
||||
<SharedText color="text-zinc-400" maxWidth="md" leading="relaxed">
|
||||
<Text variant="low" maxWidth="md">
|
||||
From casual sprints to epic endurance battles — discover the perfect league for your racing style.
|
||||
</SharedText>
|
||||
</SharedStack>
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<SharedBox display="flex" alignItems="center" gap={4}>
|
||||
<SharedBox display="flex" flexDirection="col" alignItems="end">
|
||||
<SharedText fontSize="2xl" weight="bold" color="text-white" font="mono">{viewData.leagues.length}</SharedText>
|
||||
<SharedText weight="bold" color="text-zinc-500" uppercase letterSpacing="widest" fontSize="10px">Active Leagues</SharedText>
|
||||
</SharedBox>
|
||||
<SharedBox w="px" h="8" bg="zinc-800" />
|
||||
<SharedButton
|
||||
<Group direction="row" align="center" gap={4}>
|
||||
<Group direction="column" align="end">
|
||||
<Text size="2xl" weight="bold" font="mono">{viewData.leagues.length}</Text>
|
||||
<Text weight="bold" variant="low" uppercase letterSpacing="widest" size="xs">Active Leagues</Text>
|
||||
</Group>
|
||||
<StatusDot intent="telemetry" size="lg" />
|
||||
<Button
|
||||
onClick={onCreateLeague}
|
||||
variant="primary"
|
||||
size="lg"
|
||||
icon={<Plus size={16} />}
|
||||
>
|
||||
<SharedStack direction="row" align="center" gap={2}>
|
||||
<Plus size={16} />
|
||||
Create League
|
||||
</SharedStack>
|
||||
</SharedButton>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
Create League
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{/* Search & Filters */}
|
||||
<SharedBox as="section" display="flex" flexDirection="col" gap={8} mb={12}>
|
||||
<Group direction="column" gap={8} fullWidth>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search leagues by name, description, or game..."
|
||||
@@ -123,35 +112,29 @@ export function LeaguesTemplate({
|
||||
icon={<Search size={20} />}
|
||||
/>
|
||||
|
||||
<SharedBox as="nav" display="flex" flexWrap="wrap" gap={2}>
|
||||
<Group direction="row" wrap gap={2} fullWidth>
|
||||
{categories.map((category) => {
|
||||
const isActive = activeCategory === category.id;
|
||||
const CategoryIcon = category.icon;
|
||||
return (
|
||||
<SharedButton
|
||||
<Button
|
||||
key={category.id}
|
||||
onClick={() => onCategoryChange(category.id)}
|
||||
variant={isActive ? 'primary' : 'secondary'}
|
||||
size="sm"
|
||||
icon={<CategoryIcon size={14} />}
|
||||
>
|
||||
<SharedStack direction="row" align="center" gap={2}>
|
||||
<SharedBox
|
||||
color={!isActive && category.color ? category.color : undefined}
|
||||
>
|
||||
<CategoryIcon size={14} />
|
||||
</SharedBox>
|
||||
<SharedText>{category.label}</SharedText>
|
||||
</SharedStack>
|
||||
</SharedButton>
|
||||
{category.label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{/* Grid */}
|
||||
<SharedBox as="main">
|
||||
<Group direction="column" fullWidth>
|
||||
{filteredLeagues.length > 0 ? (
|
||||
<SharedBox display="grid" responsiveGridCols={{ base: 1, md: 2, lg: 3 }} gap={6}>
|
||||
<Grid cols={{ base: 1, md: 2, lg: 3 }} gap={6}>
|
||||
{filteredLeagues.map((league) => (
|
||||
<LeagueCard
|
||||
key={league.id}
|
||||
@@ -159,25 +142,25 @@ export function LeaguesTemplate({
|
||||
onClick={() => onLeagueClick(league.id)}
|
||||
/>
|
||||
))}
|
||||
</SharedBox>
|
||||
</Grid>
|
||||
) : (
|
||||
<SharedBox display="flex" flexDirection="col" alignItems="center" justifyContent="center" py={24} border borderStyle="dashed" borderColor="zinc-800" bg="zinc-900/20">
|
||||
<SharedBox color="text-zinc-800" mb={4}>
|
||||
<Search size={48} />
|
||||
</SharedBox>
|
||||
<Heading level={3} fontSize="xl" weight="bold" color="text-zinc-500">No Leagues Found</Heading>
|
||||
<SharedText color="text-zinc-600" size="sm" mt={2}>Try adjusting your search or filters</SharedText>
|
||||
<SharedButton
|
||||
variant="ghost"
|
||||
style={{ marginTop: '1.5rem' }}
|
||||
onClick={onClearFilters}
|
||||
>
|
||||
Clear All Filters
|
||||
</SharedButton>
|
||||
</SharedBox>
|
||||
<Section variant="dark" padding="lg">
|
||||
<Group direction="column" align="center" justify="center" fullWidth>
|
||||
<Icon icon={Search} size={12} intent="low" />
|
||||
<Heading level={3} weight="bold">No Leagues Found</Heading>
|
||||
<Text variant="low" size="sm">Try adjusting your search or filters</Text>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onClearFilters}
|
||||
style={{ marginTop: '1.5rem' }}
|
||||
>
|
||||
Clear All Filters
|
||||
</Button>
|
||||
</Group>
|
||||
</Section>
|
||||
)}
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</SharedBox>
|
||||
</Group>
|
||||
</Group>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { UploadDropzone } from '@/ui/UploadDropzone';
|
||||
import { UploadDropzone } from '@/components/shared/UploadDropzone';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import {
|
||||
SharedBox,
|
||||
|
||||
@@ -39,7 +39,7 @@ export function ProfileTemplate({
|
||||
return (
|
||||
<Stack align="center" gap={4} mb={8}>
|
||||
<Surface variant="muted" rounded="xl" border padding={4}>
|
||||
<Icon icon={User} size={8} color="#3b82f6" />
|
||||
<Icon icon={User} size={8} color="var(--color-primary)" />
|
||||
</Surface>
|
||||
<Box>
|
||||
<Heading level={1}>Create Your Driver Profile</Heading>
|
||||
@@ -93,7 +93,7 @@ export function ProfileTemplate({
|
||||
{viewData.teamMemberships.length > 0 && (
|
||||
<Box as="section" aria-labelledby="teams-heading">
|
||||
<Stack gap={4}>
|
||||
<Heading level={3} id="teams-heading" fontSize="1.125rem">Teams</Heading>
|
||||
<Heading level={3} id="teams-heading">Teams</Heading>
|
||||
<TeamMembershipGrid
|
||||
memberships={viewData.teamMemberships.map(m => ({
|
||||
team: { id: m.teamId, name: m.teamName },
|
||||
@@ -109,8 +109,8 @@ export function ProfileTemplate({
|
||||
<Box as="section" aria-labelledby="achievements-heading">
|
||||
<Stack gap={4}>
|
||||
<Stack direction="row" justify="between" align="center">
|
||||
<Heading level={3} id="achievements-heading" fontSize="1.125rem">Achievements</Heading>
|
||||
<Text size="sm" color="#6b7280">{viewData.extendedProfile.achievements.length} earned</Text>
|
||||
<Heading level={3} id="achievements-heading">Achievements</Heading>
|
||||
<Text size="sm" color="text-gray-500">{viewData.extendedProfile.achievements.length} earned</Text>
|
||||
</Stack>
|
||||
<AchievementGrid
|
||||
achievements={viewData.extendedProfile.achievements.map(a => ({
|
||||
@@ -128,7 +128,7 @@ export function ProfileTemplate({
|
||||
{activeTab === 'history' && (
|
||||
<Box as="section" aria-labelledby="history-heading">
|
||||
<Stack gap={4}>
|
||||
<Heading level={3} id="history-heading" fontSize="1.125rem">Race History</Heading>
|
||||
<Heading level={3} id="history-heading">Race History</Heading>
|
||||
<Card>
|
||||
<SessionHistoryTable results={[]} />
|
||||
</Card>
|
||||
@@ -139,14 +139,14 @@ export function ProfileTemplate({
|
||||
{activeTab === 'stats' && viewData.stats && (
|
||||
<Box as="section" aria-labelledby="stats-heading">
|
||||
<Stack gap={4}>
|
||||
<Heading level={3} id="stats-heading" fontSize="1.125rem">Performance Overview</Heading>
|
||||
<Heading level={3} id="stats-heading">Performance Overview</Heading>
|
||||
<Card>
|
||||
<ProfileStatGrid
|
||||
stats={[
|
||||
{ label: 'Races', value: viewData.stats.totalRacesLabel },
|
||||
{ label: 'Wins', value: viewData.stats.winsLabel, color: '#10b981' },
|
||||
{ label: 'Podiums', value: viewData.stats.podiumsLabel, color: '#f59e0b' },
|
||||
{ label: 'Consistency', value: viewData.stats.consistencyLabel, color: '#3b82f6' },
|
||||
{ label: 'Wins', value: viewData.stats.winsLabel, intent: 'success' },
|
||||
{ label: 'Podiums', value: viewData.stats.podiumsLabel, intent: 'telemetry' },
|
||||
{ label: 'Consistency', value: viewData.stats.consistencyLabel, intent: 'primary' },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -8,12 +8,13 @@ import { RaceScheduleTable } from '@/components/races/RaceScheduleTable';
|
||||
import { RaceSidebar } from '@/components/races/RaceSidebar';
|
||||
import type { SessionStatus } from '@/components/races/SessionStatusBadge';
|
||||
import type { RacesViewData } from '@/lib/view-data/RacesViewData';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { GridItem } from '@/ui/GridItem';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Group } from '@/ui/Group';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Panel } from '@/ui/Panel';
|
||||
import React from 'react';
|
||||
|
||||
export type TimeFilter = 'all' | 'upcoming' | 'live' | 'past';
|
||||
export type RaceStatusFilter = 'scheduled' | 'running' | 'completed' | 'cancelled' | 'all';
|
||||
@@ -50,78 +51,77 @@ export function RacesTemplate({
|
||||
setShowFilterModal,
|
||||
}: RacesTemplateProps) {
|
||||
return (
|
||||
<Box as="main" minHeight="screen" bg="bg-base-black" py={8}>
|
||||
<Container size="lg">
|
||||
<Stack gap={8}>
|
||||
<RacePageHeader
|
||||
totalCount={viewData.totalCount}
|
||||
scheduledCount={viewData.scheduledCount}
|
||||
runningCount={viewData.runningCount}
|
||||
completedCount={viewData.completedCount}
|
||||
/>
|
||||
<Container size="lg" py={8}>
|
||||
<Group direction="column" gap={8} fullWidth>
|
||||
<RacePageHeader
|
||||
totalCount={viewData.totalCount}
|
||||
scheduledCount={viewData.scheduledCount}
|
||||
runningCount={viewData.runningCount}
|
||||
completedCount={viewData.completedCount}
|
||||
/>
|
||||
|
||||
<LiveRacesBanner
|
||||
liveRaces={viewData.liveRaces}
|
||||
onRaceClick={onRaceClick}
|
||||
/>
|
||||
<LiveRacesBanner
|
||||
liveRaces={viewData.liveRaces}
|
||||
onRaceClick={onRaceClick}
|
||||
/>
|
||||
|
||||
<Grid cols={12} gap={6}>
|
||||
<GridItem colSpan={12} lgSpan={8}>
|
||||
<Stack gap={6}>
|
||||
<RaceFilterBar
|
||||
timeFilter={timeFilter}
|
||||
setTimeFilter={setTimeFilter}
|
||||
leagueFilter={leagueFilter}
|
||||
setLeagueFilter={setLeagueFilter}
|
||||
leagues={viewData.leagues}
|
||||
onShowMoreFilters={() => setShowFilterModal(true)}
|
||||
/>
|
||||
|
||||
<Box as="section" bg="bg-surface-charcoal" border borderColor="border-outline-steel" overflow="hidden">
|
||||
<Box p={4} borderBottom borderColor="border-outline-steel" bg="bg-base-black" bgOpacity={0.2}>
|
||||
<Text size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="widest">Race Schedule</Text>
|
||||
</Box>
|
||||
<RaceScheduleTable
|
||||
races={viewData.races.map(race => ({
|
||||
id: race.id,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
leagueName: race.leagueName,
|
||||
time: race.timeLabel,
|
||||
status: race.status as SessionStatus
|
||||
}))}
|
||||
onRaceClick={onRaceClick}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</GridItem>
|
||||
|
||||
<GridItem colSpan={12} lgSpan={4}>
|
||||
<RaceSidebar
|
||||
upcomingRaces={viewData.upcomingRaces}
|
||||
recentResults={viewData.recentResults}
|
||||
onRaceClick={onRaceClick}
|
||||
<Grid cols={12} gap={6}>
|
||||
<GridItem colSpan={12} lgSpan={8}>
|
||||
<Group direction="column" gap={6} fullWidth>
|
||||
<RaceFilterBar
|
||||
timeFilter={timeFilter}
|
||||
setTimeFilter={setTimeFilter}
|
||||
leagueFilter={leagueFilter}
|
||||
setLeagueFilter={setLeagueFilter}
|
||||
leagues={viewData.leagues}
|
||||
onShowMoreFilters={() => setShowFilterModal(true)}
|
||||
/>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
|
||||
<RaceFilterModal
|
||||
isOpen={showFilterModal}
|
||||
onClose={() => setShowFilterModal(false)}
|
||||
statusFilter={statusFilter}
|
||||
setStatusFilter={setStatusFilter}
|
||||
leagueFilter={leagueFilter}
|
||||
setLeagueFilter={setLeagueFilter}
|
||||
timeFilter={timeFilter}
|
||||
setTimeFilter={setTimeFilter}
|
||||
searchQuery=""
|
||||
setSearchQuery={() => {}}
|
||||
leagues={viewData.leagues}
|
||||
showSearch={false}
|
||||
showTimeFilter={false}
|
||||
/>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Box>
|
||||
<Panel
|
||||
title="Race Schedule"
|
||||
variant="dark"
|
||||
padding={0}
|
||||
>
|
||||
<RaceScheduleTable
|
||||
races={viewData.races.map(race => ({
|
||||
id: race.id,
|
||||
track: race.track,
|
||||
car: race.car,
|
||||
leagueName: race.leagueName,
|
||||
time: race.timeLabel,
|
||||
status: race.status as SessionStatus
|
||||
}))}
|
||||
onRaceClick={onRaceClick}
|
||||
/>
|
||||
</Panel>
|
||||
</Group>
|
||||
</GridItem>
|
||||
|
||||
<GridItem colSpan={12} lgSpan={4}>
|
||||
<RaceSidebar
|
||||
upcomingRaces={viewData.upcomingRaces}
|
||||
recentResults={viewData.recentResults}
|
||||
onRaceClick={onRaceClick}
|
||||
/>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
|
||||
<RaceFilterModal
|
||||
isOpen={showFilterModal}
|
||||
onClose={() => setShowFilterModal(false)}
|
||||
statusFilter={statusFilter}
|
||||
setStatusFilter={setStatusFilter}
|
||||
leagueFilter={leagueFilter}
|
||||
setLeagueFilter={setLeagueFilter}
|
||||
timeFilter={timeFilter}
|
||||
setTimeFilter={setTimeFilter}
|
||||
searchQuery=""
|
||||
setSearchQuery={() => {}}
|
||||
leagues={viewData.leagues}
|
||||
showSearch={false}
|
||||
showTimeFilter={false}
|
||||
/>
|
||||
</Group>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,11 +42,11 @@ export function RosterAdminTemplate({
|
||||
<Box>
|
||||
<Stack direction="row" align="center" justify="between" mb={4}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={UserPlus} size={4} color="text-primary-blue" />
|
||||
<Heading level={5} color="text-primary-blue">PENDING JOIN REQUESTS</Heading>
|
||||
<Icon icon={UserPlus} size={4} color="primary-accent" />
|
||||
<Heading level={5} color="primary-accent">PENDING JOIN REQUESTS</Heading>
|
||||
</Stack>
|
||||
<Box px={2} py={0.5} rounded="md" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20">
|
||||
<Text size="xs" color="text-primary-blue" weight="bold">{pendingCountLabel}</Text>
|
||||
<Box px={2} py={0.5} rounded="md" bg="rgba(25,140,255,0.1)" border borderColor="rgba(25,140,255,0.2)">
|
||||
<Text size="xs" color="primary-accent" weight="bold">{pendingCountLabel}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -58,13 +58,13 @@ export function RosterAdminTemplate({
|
||||
<Surface variant="dark" border rounded="lg" overflow="hidden">
|
||||
<Stack gap={0}>
|
||||
{joinRequests.map((req) => (
|
||||
<Box key={req.id} p={4} borderBottom borderColor="border-charcoal-outline" hoverBg="bg-white/5" transition>
|
||||
<Box key={req.id} p={4} borderBottom borderColor="rgba(255,255,255,0.1)">
|
||||
<Stack direction={{ base: 'col', md: 'row' }} align="center" justify="between" gap={4}>
|
||||
<Stack gap={1}>
|
||||
<Text weight="bold" color="text-white">{req.driver.name}</Text>
|
||||
<Text weight="bold" color="white">{req.driver.name}</Text>
|
||||
<Text size="xs" color="text-gray-500">{req.formattedRequestedAt}</Text>
|
||||
{req.message && (
|
||||
<Text size="sm" color="text-gray-400" mt={1}>"{req.message}"</Text>
|
||||
<Text size="sm" color="text-gray-400" mt={1}>"{req.message}"</Text>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -91,8 +91,8 @@ export function RosterAdminTemplate({
|
||||
{/* Members Section */}
|
||||
<Box>
|
||||
<Stack direction="row" align="center" gap={2} mb={4}>
|
||||
<Icon icon={Shield} size={4} color="text-performance-green" />
|
||||
<Heading level={5} color="text-performance-green">ACTIVE ROSTER</Heading>
|
||||
<Icon icon={Shield} size={4} color="text-success-green" />
|
||||
<Heading level={5} color="text-success-green">ACTIVE ROSTER</Heading>
|
||||
</Stack>
|
||||
|
||||
{loading ? (
|
||||
@@ -114,7 +114,7 @@ export function RosterAdminTemplate({
|
||||
{members.map((member) => (
|
||||
<TableRow key={member.driverId}>
|
||||
<TableCell>
|
||||
<Text weight="bold" color="text-white">{member.driver.name}</Text>
|
||||
<Text weight="bold" color="white">{member.driver.name}</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text size="sm" color="text-gray-400">{member.formattedJoinedAt}</Text>
|
||||
@@ -124,15 +124,13 @@ export function RosterAdminTemplate({
|
||||
as="select"
|
||||
value={member.role}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => onRoleChange(member.driverId, e.target.value as MembershipRole)}
|
||||
bg="bg-iron-gray"
|
||||
bg="rgba(255,255,255,0.05)"
|
||||
border
|
||||
borderColor="border-charcoal-outline"
|
||||
borderColor="rgba(255,255,255,0.1)"
|
||||
rounded="md"
|
||||
px={2}
|
||||
py={1}
|
||||
fontSize="xs"
|
||||
weight="bold"
|
||||
color="text-white"
|
||||
color="white"
|
||||
>
|
||||
{roleOptions.map((role) => (
|
||||
<Box as="option" key={role} value={role}>{role.toUpperCase()}</Box>
|
||||
@@ -146,8 +144,8 @@ export function RosterAdminTemplate({
|
||||
size="sm"
|
||||
>
|
||||
<Stack direction="row" align="center" gap={1.5}>
|
||||
<Icon icon={UserMinus} size={3.5} color="text-error-red" />
|
||||
<Text size="xs" weight="bold" color="text-error-red">REMOVE</Text>
|
||||
<Icon icon={UserMinus} size={3.5} color="critical-red" />
|
||||
<Text size="xs" weight="bold" color="critical-red">REMOVE</Text>
|
||||
</Stack>
|
||||
</Button>
|
||||
</TableCell>
|
||||
|
||||
@@ -17,8 +17,8 @@ import { Heading } from '@/ui/Heading';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { GridItem } from '@/ui/GridItem';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import {
|
||||
Bell,
|
||||
Car,
|
||||
@@ -81,7 +81,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
|
||||
description: a.formattedImpressions ? `${a.formattedImpressions} impressions` : '',
|
||||
timestamp: a.time,
|
||||
icon: Clock,
|
||||
color: a.typeColor || 'text-primary-blue',
|
||||
color: a.typeColor || 'primary-accent',
|
||||
}));
|
||||
|
||||
return (
|
||||
@@ -97,7 +97,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
|
||||
<BillingSummaryPanel stats={billingStats} />
|
||||
|
||||
{/* Key Metrics */}
|
||||
<Grid cols={4} gap={4}>
|
||||
<Grid responsiveGridCols={{ base: 1, sm: 2, lg: 4 }} gap={4}>
|
||||
<MetricCard
|
||||
title="Total Impressions"
|
||||
value={viewData.totalImpressions}
|
||||
@@ -130,27 +130,30 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
|
||||
</Grid>
|
||||
|
||||
{/* Main Content Grid */}
|
||||
<Grid cols={12} gap={6}>
|
||||
<GridItem colSpan={12} lgSpan={8}>
|
||||
<Grid responsiveGridCols={{ base: 1, lg: 12 }} gap={6}>
|
||||
<Box responsiveColSpan={{ base: 1, lg: 8 }}>
|
||||
<Stack gap={6}>
|
||||
{/* Sponsorship Categories */}
|
||||
<Box>
|
||||
<Stack direction="row" align="center" justify="between" mb={4}>
|
||||
<Heading level={3}>Your Sponsorships</Heading>
|
||||
<Link href={routes.sponsor.campaigns}>
|
||||
<Button variant="secondary" size="sm" icon={<Icon icon={ChevronRight} size={4} />}>
|
||||
View All
|
||||
<Button variant="secondary" size="sm">
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Text>View All</Text>
|
||||
<Icon icon={ChevronRight} size={4} />
|
||||
</Stack>
|
||||
</Button>
|
||||
</Link>
|
||||
</Stack>
|
||||
|
||||
<Grid cols={5} gap={4}>
|
||||
<Grid responsiveGridCols={{ base: 2, md: 3, lg: 5 }} gap={4}>
|
||||
<SponsorshipCategoryCard
|
||||
icon={Trophy}
|
||||
title="Leagues"
|
||||
countLabel={categoryData.leagues.countLabel}
|
||||
impressionsLabel={categoryData.leagues.impressionsLabel}
|
||||
color="#3b82f6"
|
||||
color="primary-accent"
|
||||
href="/sponsor/campaigns?type=leagues"
|
||||
/>
|
||||
<SponsorshipCategoryCard
|
||||
@@ -158,7 +161,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
|
||||
title="Teams"
|
||||
countLabel={categoryData.teams.countLabel}
|
||||
impressionsLabel={categoryData.teams.impressionsLabel}
|
||||
color="#a855f7"
|
||||
color="var(--color-warning)"
|
||||
href="/sponsor/campaigns?type=teams"
|
||||
/>
|
||||
<SponsorshipCategoryCard
|
||||
@@ -166,7 +169,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
|
||||
title="Drivers"
|
||||
countLabel={categoryData.drivers.countLabel}
|
||||
impressionsLabel={categoryData.drivers.impressionsLabel}
|
||||
color="#10b981"
|
||||
color="var(--color-success)"
|
||||
href="/sponsor/campaigns?type=drivers"
|
||||
/>
|
||||
<SponsorshipCategoryCard
|
||||
@@ -174,7 +177,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
|
||||
title="Races"
|
||||
countLabel={categoryData.races.countLabel}
|
||||
impressionsLabel={categoryData.races.impressionsLabel}
|
||||
color="#f59e0b"
|
||||
color="var(--color-warning)"
|
||||
href="/sponsor/campaigns?type=races"
|
||||
/>
|
||||
<SponsorshipCategoryCard
|
||||
@@ -182,7 +185,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
|
||||
title="Platform Ads"
|
||||
countLabel={categoryData.platform.countLabel}
|
||||
impressionsLabel={categoryData.platform.impressionsLabel}
|
||||
color="#ef4444"
|
||||
color="critical-red"
|
||||
href="/sponsor/campaigns?type=platform"
|
||||
/>
|
||||
</Grid>
|
||||
@@ -193,12 +196,15 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
|
||||
<Stack direction="row" align="center" justify="between" mb={4}>
|
||||
<Heading level={3}>Top Performing</Heading>
|
||||
<Link href={routes.public.leagues}>
|
||||
<Button variant="secondary" size="sm" icon={<Icon icon={Plus} size={4} />}>
|
||||
Find More
|
||||
<Button variant="secondary" size="sm">
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={Plus} size={4} />
|
||||
<Text>Find More</Text>
|
||||
</Stack>
|
||||
</Button>
|
||||
</Link>
|
||||
</Stack>
|
||||
<Grid cols={2} gap={4}>
|
||||
<Grid responsiveGridCols={{ base: 1, md: 2 }} gap={4}>
|
||||
<SponsorContractCard
|
||||
id="sample-1"
|
||||
type="league"
|
||||
@@ -226,9 +232,9 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
|
||||
</Grid>
|
||||
</Box>
|
||||
</Stack>
|
||||
</GridItem>
|
||||
</Box>
|
||||
|
||||
<GridItem colSpan={12} lgSpan={4}>
|
||||
<Box responsiveColSpan={{ base: 1, lg: 4 }}>
|
||||
<Stack gap={6}>
|
||||
{/* Recent Activity */}
|
||||
<SponsorActivityPanel activities={activities} />
|
||||
@@ -239,23 +245,35 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
|
||||
<Heading level={3}>Quick Actions</Heading>
|
||||
<Stack gap={2}>
|
||||
<Link href={routes.public.leagues}>
|
||||
<Button variant="secondary" fullWidth icon={<Icon icon={Trophy} size={4} />}>
|
||||
Find Leagues to Sponsor
|
||||
<Button variant="secondary" fullWidth>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={Trophy} size={4} />
|
||||
<Text>Find Leagues to Sponsor</Text>
|
||||
</Stack>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href={routes.public.teams}>
|
||||
<Button variant="secondary" fullWidth icon={<Icon icon={Users} size={4} />}>
|
||||
Browse Teams
|
||||
<Button variant="secondary" fullWidth>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={Users} size={4} />
|
||||
<Text>Browse Teams</Text>
|
||||
</Stack>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href={routes.public.drivers}>
|
||||
<Button variant="secondary" fullWidth icon={<Icon icon={Car} size={4} />}>
|
||||
Discover Drivers
|
||||
<Button variant="secondary" fullWidth>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={Car} size={4} />
|
||||
<Text>Discover Drivers</Text>
|
||||
</Stack>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href={routes.sponsor.billing}>
|
||||
<Button variant="secondary" fullWidth icon={<Icon icon={DollarSign} size={4} />}>
|
||||
Manage Billing
|
||||
<Button variant="secondary" fullWidth>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={DollarSign} size={4} />
|
||||
<Text>Manage Billing</Text>
|
||||
</Stack>
|
||||
</Button>
|
||||
</Link>
|
||||
</Stack>
|
||||
@@ -266,8 +284,11 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
|
||||
{viewData.upcomingRenewals.length > 0 && (
|
||||
<Card>
|
||||
<Stack gap={4}>
|
||||
<Heading level={3} icon={<Icon icon={Bell} size={5} color="#f59e0b" />}>
|
||||
Upcoming Renewals
|
||||
<Heading level={3}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Icon icon={Bell} size={5} color="warning-amber" />
|
||||
<Text>Upcoming Renewals</Text>
|
||||
</Stack>
|
||||
</Heading>
|
||||
<Stack gap={3}>
|
||||
{viewData.upcomingRenewals.map((renewal) => (
|
||||
@@ -278,7 +299,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
|
||||
</Card>
|
||||
)}
|
||||
</Stack>
|
||||
</GridItem>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Container>
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
|
||||
import { LeaderboardFiltersBar } from '@/components/leaderboards/LeaderboardFiltersBar';
|
||||
import type { SkillLevel, SortBy, TeamLeaderboardViewData } from '@/lib/view-data/TeamLeaderboardViewData';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Group } from '@/ui/Group';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/ui/Table';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Panel } from '@/ui/Panel';
|
||||
import { Section } from '@/ui/Section';
|
||||
import { Award, ChevronLeft } from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
interface TeamLeaderboardTemplateProps {
|
||||
viewData: TeamLeaderboardViewData;
|
||||
@@ -30,87 +32,88 @@ export function TeamLeaderboardTemplate({
|
||||
const { searchQuery, filteredAndSortedTeams } = viewData;
|
||||
|
||||
return (
|
||||
<Box bg="base-black" minH="screen">
|
||||
<Container size="lg" py={12}>
|
||||
<Stack gap={8}>
|
||||
{/* Header */}
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Stack direction="row" align="center" gap={4}>
|
||||
<Button variant="secondary" size="sm" onClick={onBackToTeams} icon={<Icon icon={ChevronLeft} size={4} />}>
|
||||
Back
|
||||
</Button>
|
||||
<Box>
|
||||
<Heading level={1} weight="bold">Global Standings</Heading>
|
||||
<Text color="text-gray-500" size="sm" font="mono" uppercase letterSpacing="widest">Team Performance Index</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Icon icon={Award} size={8} color="warning-amber" />
|
||||
</Stack>
|
||||
<Container size="lg" py={12}>
|
||||
<Group direction="column" gap={8} fullWidth>
|
||||
{/* Header */}
|
||||
<Group direction="row" align="center" justify="between" fullWidth>
|
||||
<Group direction="row" align="center" gap={4}>
|
||||
<Button variant="secondary" size="sm" onClick={onBackToTeams} icon={<Icon icon={ChevronLeft} size={4} />}>
|
||||
Back
|
||||
</Button>
|
||||
<Group direction="column">
|
||||
<Heading level={1} weight="bold">Global Standings</Heading>
|
||||
<Text variant="low" size="sm" font="mono" uppercase letterSpacing="widest">Team Performance Index</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
<Icon icon={Award} size={8} color="var(--ui-color-intent-warning)" />
|
||||
</Group>
|
||||
|
||||
<LeaderboardFiltersBar
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={onSearchChange}
|
||||
placeholder="Search teams..."
|
||||
/>
|
||||
<LeaderboardFiltersBar
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={onSearchChange}
|
||||
placeholder="Search teams..."
|
||||
/>
|
||||
|
||||
<Box border borderColor="outline-steel" bg="surface-charcoal/30">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeader w="20">Rank</TableHeader>
|
||||
<TableHeader>Team</TableHeader>
|
||||
<TableHeader textAlign="center">Personnel</TableHeader>
|
||||
<TableHeader textAlign="center">Races</TableHeader>
|
||||
<TableHeader textAlign="right">Rating</TableHeader>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{filteredAndSortedTeams.length > 0 ? (
|
||||
filteredAndSortedTeams.map((team, index) => (
|
||||
<TableRow
|
||||
key={team.id}
|
||||
onClick={() => onTeamClick(team.id)}
|
||||
cursor="pointer"
|
||||
hoverBg="surface-charcoal/50"
|
||||
>
|
||||
<TableCell>
|
||||
<Text font="mono" weight="bold" color={index < 3 ? 'warning-amber' : 'text-gray-500'}>
|
||||
#{index + 1}
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Box w="8" h="8" bg="base-black" border borderColor="outline-steel" display="flex" center>
|
||||
<Text size="xs" weight="bold" color="primary-accent">{team.name.substring(0, 2).toUpperCase()}</Text>
|
||||
</Box>
|
||||
<Text weight="bold" size="sm" color="text-white">{team.name}</Text>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell textAlign="center">
|
||||
<Text size="xs" color="text-gray-400" font="mono">{team.memberCount}</Text>
|
||||
</TableCell>
|
||||
<TableCell textAlign="center">
|
||||
<Text size="xs" color="text-gray-400" font="mono">{team.totalRaces}</Text>
|
||||
</TableCell>
|
||||
<TableCell textAlign="right">
|
||||
<Text font="mono" weight="bold" color="primary-accent">1450</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} textAlign="center" py={12}>
|
||||
<Text color="text-gray-600" font="mono" size="xs" uppercase letterSpacing="widest">
|
||||
No teams found matching criteria
|
||||
<Panel variant="dark" padding={0}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeader w="20">Rank</TableHeader>
|
||||
<TableHeader>Team</TableHeader>
|
||||
<TableHeader textAlign="center">Personnel</TableHeader>
|
||||
<TableHeader textAlign="center">Races</TableHeader>
|
||||
<TableHeader textAlign="right">Rating</TableHeader>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{filteredAndSortedTeams.length > 0 ? (
|
||||
filteredAndSortedTeams.map((team, index) => (
|
||||
<TableRow
|
||||
key={team.id}
|
||||
onClick={() => onTeamClick(team.id)}
|
||||
clickable
|
||||
>
|
||||
<TableCell>
|
||||
<Text font="mono" weight="bold" variant={index < 3 ? 'warning' : 'low'}>
|
||||
#{index + 1}
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Group direction="row" align="center" gap={3}>
|
||||
<Panel variant="muted" padding={2}>
|
||||
<Text size="xs" weight="bold" color="text-primary-accent">{team.name.substring(0, 2).toUpperCase()}</Text>
|
||||
</Panel>
|
||||
<Text weight="bold" size="sm">{team.name}</Text>
|
||||
</Group>
|
||||
</TableCell>
|
||||
<TableCell textAlign="center">
|
||||
<Text size="xs" variant="low" font="mono">{team.memberCount}</Text>
|
||||
</TableCell>
|
||||
<TableCell textAlign="center">
|
||||
<Text size="xs" variant="low" font="mono">{team.totalRaces}</Text>
|
||||
</TableCell>
|
||||
<TableCell textAlign="right">
|
||||
<Text font="mono" weight="bold" color="text-primary-accent">1450</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Box>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} textAlign="center">
|
||||
<Section variant="dark" padding="lg">
|
||||
<Group align="center" justify="center" fullWidth>
|
||||
<Text variant="low" font="mono" size="xs" uppercase letterSpacing="widest">
|
||||
No teams found matching criteria
|
||||
</Text>
|
||||
</Group>
|
||||
</Section>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Panel>
|
||||
</Group>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ import { TeamGrid } from '@/components/teams/TeamGrid';
|
||||
import { TeamLeaderboardPreview } from '@/components/teams/TeamLeaderboardPreviewWrapper';
|
||||
import { TeamsDirectoryHeader } from '@/components/teams/TeamsDirectoryHeader';
|
||||
import { TeamsDirectory, TeamsDirectorySection } from '@/components/teams/TeamsDirectory';
|
||||
import { SharedEmptyState } from '@/components/shared/UIComponents';
|
||||
import { EmptyState } from '@/ui/EmptyState';
|
||||
import type { TeamsViewData } from '@/lib/view-data/TeamsViewData';
|
||||
import { Users } from 'lucide-react';
|
||||
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
|
||||
import React from 'react';
|
||||
|
||||
interface TeamsTemplateProps extends TemplateProps<TeamsViewData> {
|
||||
onTeamClick?: (teamId: string) => void;
|
||||
@@ -41,7 +42,7 @@ export function TeamsTemplate({ viewData, onTeamClick, onViewFullLeaderboard, on
|
||||
))}
|
||||
</TeamGrid>
|
||||
) : (
|
||||
<SharedEmptyState
|
||||
<EmptyState
|
||||
icon={Users}
|
||||
title="No teams yet"
|
||||
description="Get started by creating your first racing team"
|
||||
|
||||
@@ -5,13 +5,12 @@ import { AuthFooterLinks } from '@/components/auth/AuthFooterLinks';
|
||||
import { AuthForm } from '@/components/auth/AuthForm';
|
||||
import { ForgotPasswordViewData } from '@/lib/builders/view-data/types/ForgotPasswordViewData';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Group } from '@/ui/Group';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Input } from '@/ui/Input';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { LoadingSpinner } from '@/ui/LoadingSpinner';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { AlertCircle, ArrowLeft, CheckCircle2, Mail, Shield } from 'lucide-react';
|
||||
import React from 'react';
|
||||
@@ -54,12 +53,10 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
|
||||
/>
|
||||
|
||||
{mutationState.error && (
|
||||
<Box p={4} bg="critical-red/10" border borderColor="critical-red/30" rounded="md">
|
||||
<Stack direction="row" align="start" gap={3}>
|
||||
<Icon icon={AlertCircle} size={4.5} color="var(--color-critical)" />
|
||||
<Text size="sm" color="text-critical-red">{mutationState.error}</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Group direction="row" align="start" gap={3} fullWidth>
|
||||
<Icon icon={AlertCircle} size={4.5} color="var(--color-critical)" />
|
||||
<Text size="sm" color="text-critical-red">{mutationState.error}</Text>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
<Button
|
||||
@@ -72,36 +69,34 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
|
||||
{isSubmitting ? 'Sending...' : 'Send Reset Link'}
|
||||
</Button>
|
||||
|
||||
<Box textAlign="center">
|
||||
<Group justify="center" fullWidth>
|
||||
<Link href={routes.auth.login}>
|
||||
<Stack direction="row" align="center" justify="center" gap={2} group>
|
||||
<Icon icon={ArrowLeft} size={3.5} color="var(--color-primary)" groupHoverScale />
|
||||
<Group direction="row" align="center" justify="center" gap={2}>
|
||||
<Icon icon={ArrowLeft} size={3.5} color="var(--color-primary)" />
|
||||
<Text size="sm" weight="bold" color="text-primary-accent">Back to Login</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Link>
|
||||
</Box>
|
||||
</Group>
|
||||
</AuthForm>
|
||||
) : (
|
||||
<Stack gap={6}>
|
||||
<Box p={4} bg="success-green/10" border borderColor="success-green/30" rounded="md">
|
||||
<Stack direction="row" align="start" gap={3}>
|
||||
<Icon icon={CheckCircle2} size={5} color="var(--color-success)" />
|
||||
<Box>
|
||||
<Text size="sm" color="text-success-green" weight="bold" block>Check your email</Text>
|
||||
<Text size="xs" color="text-gray-400" block mt={1}>{viewData.successMessage}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Group direction="column" gap={6} fullWidth>
|
||||
<Group direction="row" align="start" gap={3} fullWidth>
|
||||
<Icon icon={CheckCircle2} size={5} color="var(--color-success)" />
|
||||
<Group direction="column" gap={1}>
|
||||
<Text size="sm" color="text-success-green" weight="bold" block>Check your email</Text>
|
||||
<Text size="xs" color="text-gray-400" block>{viewData.successMessage}</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{viewData.magicLink && (
|
||||
<Box p={3} bg="surface-charcoal" border borderColor="outline-steel" rounded="md">
|
||||
<Text size="xs" color="text-gray-500" block mb={2} weight="bold">DEVELOPMENT MAGIC LINK</Text>
|
||||
<Group direction="column" gap={2} fullWidth>
|
||||
<Text size="xs" color="text-gray-500" block weight="bold">DEVELOPMENT MAGIC LINK</Text>
|
||||
<Link href={viewData.magicLink}>
|
||||
<Text size="xs" color="text-primary-accent" block>
|
||||
{viewData.magicLink}
|
||||
</Text>
|
||||
</Link>
|
||||
</Box>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
<Button
|
||||
@@ -112,7 +107,7 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
|
||||
>
|
||||
Return to Login
|
||||
</Button>
|
||||
</Stack>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
<AuthFooterLinks>
|
||||
|
||||
@@ -7,14 +7,14 @@ import { EnhancedFormError } from '@/components/errors/EnhancedFormError';
|
||||
import { FormState } from '@/lib/builders/view-data/types/FormState';
|
||||
import { LoginViewData } from '@/lib/builders/view-data/types/LoginViewData';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Checkbox } from '@/ui/Checkbox';
|
||||
import { Group } from '@/ui/Group';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Input } from '@/ui/Input';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { LoadingSpinner } from '@/ui/LoadingSpinner';
|
||||
import { PasswordField } from '@/ui/PasswordField';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { AlertCircle, LogIn, Mail } from 'lucide-react';
|
||||
import React from 'react';
|
||||
@@ -43,7 +43,7 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
|
||||
description="Sign in to access your racing dashboard"
|
||||
>
|
||||
<AuthForm onSubmit={formActions.handleSubmit}>
|
||||
<Stack gap={4}>
|
||||
<Group direction="column" gap={4} fullWidth>
|
||||
<Input
|
||||
label="Email Address"
|
||||
id="email"
|
||||
@@ -58,7 +58,7 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
|
||||
icon={<Mail size={16} />}
|
||||
/>
|
||||
|
||||
<Stack gap={1.5}>
|
||||
<Group direction="column" gap={1.5} fullWidth>
|
||||
<PasswordField
|
||||
label="Password"
|
||||
id="password"
|
||||
@@ -72,50 +72,43 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
|
||||
showPassword={viewData.showPassword}
|
||||
onTogglePassword={() => formActions.setShowPassword(!viewData.showPassword)}
|
||||
/>
|
||||
<Box textAlign="right">
|
||||
<Group justify="end" fullWidth>
|
||||
<Link href={routes.auth.forgotPassword}>
|
||||
<Text size="xs" color="text-primary-accent">
|
||||
Forgot password?
|
||||
</Text>
|
||||
</Link>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Box
|
||||
as="input"
|
||||
id="rememberMe"
|
||||
name="rememberMe"
|
||||
type="checkbox"
|
||||
rounded="sm"
|
||||
borderColor="outline-steel"
|
||||
bg="surface-charcoal"
|
||||
color="text-primary-accent"
|
||||
ring="focus:ring-primary-accent/50"
|
||||
w="1rem"
|
||||
h="1rem"
|
||||
checked={viewData.formState.fields.rememberMe.value as boolean}
|
||||
onChange={formActions.handleChange}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<Text as="label" htmlFor="rememberMe" size="sm" color="text-med" cursor="pointer">
|
||||
Keep me signed in
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Checkbox
|
||||
label="Keep me signed in"
|
||||
checked={viewData.formState.fields.rememberMe.value as boolean}
|
||||
onChange={(checked) => {
|
||||
const event = {
|
||||
target: {
|
||||
name: 'rememberMe',
|
||||
value: checked,
|
||||
type: 'checkbox',
|
||||
checked
|
||||
}
|
||||
} as any;
|
||||
formActions.handleChange(event);
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{viewData.hasInsufficientPermissions && (
|
||||
<Box p={4} bg="warning-amber/10" border borderColor="warning-amber/30" rounded="md">
|
||||
<Stack direction="row" align="start" gap={3}>
|
||||
<Icon icon={AlertCircle} size={5} color="var(--color-warning)" />
|
||||
<Box>
|
||||
<Text weight="bold" color="text-warning-amber" block size="sm">Insufficient Permissions</Text>
|
||||
<Text size="xs" color="text-gray-400" block mt={1}>
|
||||
Please log in with an account that has the required role.
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Group direction="row" align="start" gap={3} fullWidth>
|
||||
<Icon icon={AlertCircle} size={5} color="var(--color-warning)" />
|
||||
<Group direction="column" gap={1}>
|
||||
<Text weight="bold" color="text-warning-amber" block size="sm">Insufficient Permissions</Text>
|
||||
<Text size="xs" color="text-gray-400" block>
|
||||
Please log in with an account that has the required role.
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{viewData.submitError && (
|
||||
@@ -149,14 +142,14 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
|
||||
</Link>
|
||||
</Text>
|
||||
|
||||
<Box mt={2}>
|
||||
<Group direction="column" gap={1} align="center" fullWidth>
|
||||
<Text size="xs" color="text-gray-600">
|
||||
By signing in, you agree to our{' '}
|
||||
<Link href="/terms">Terms</Link>
|
||||
{' '}and{' '}
|
||||
<Link href="/privacy">Privacy</Link>
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
</AuthFooterLinks>
|
||||
</AuthCard>
|
||||
);
|
||||
|
||||
@@ -5,13 +5,12 @@ import { AuthFooterLinks } from '@/components/auth/AuthFooterLinks';
|
||||
import { AuthForm } from '@/components/auth/AuthForm';
|
||||
import { ResetPasswordViewData } from '@/lib/builders/view-data/types/ResetPasswordViewData';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Group } from '@/ui/Group';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { LoadingSpinner } from '@/ui/LoadingSpinner';
|
||||
import { PasswordField } from '@/ui/PasswordField';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { AlertCircle, ArrowLeft, CheckCircle2, Shield } from 'lucide-react';
|
||||
import React from 'react';
|
||||
@@ -50,7 +49,7 @@ export function ResetPasswordTemplate({
|
||||
>
|
||||
{!viewData.showSuccess ? (
|
||||
<AuthForm onSubmit={formActions.handleSubmit}>
|
||||
<Stack gap={4}>
|
||||
<Group direction="column" gap={4} fullWidth>
|
||||
<PasswordField
|
||||
label="New Password"
|
||||
id="newPassword"
|
||||
@@ -78,15 +77,13 @@ export function ResetPasswordTemplate({
|
||||
showPassword={uiState.showConfirmPassword}
|
||||
onTogglePassword={() => formActions.setShowConfirmPassword(!uiState.showConfirmPassword)}
|
||||
/>
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
{mutationState.error && (
|
||||
<Box p={4} bg="critical-red/10" border borderColor="critical-red/30" rounded="md">
|
||||
<Stack direction="row" align="start" gap={3}>
|
||||
<Icon icon={AlertCircle} size={4.5} color="var(--color-critical)" />
|
||||
<Text size="sm" color="text-critical-red">{mutationState.error}</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Group direction="row" align="start" gap={3} fullWidth>
|
||||
<Icon icon={AlertCircle} size={4.5} color="var(--color-critical)" />
|
||||
<Text size="sm" color="text-critical-red">{mutationState.error}</Text>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
<Button
|
||||
@@ -99,26 +96,24 @@ export function ResetPasswordTemplate({
|
||||
{isSubmitting ? 'Resetting...' : 'Reset Password'}
|
||||
</Button>
|
||||
|
||||
<Box textAlign="center">
|
||||
<Group justify="center" fullWidth>
|
||||
<Link href={routes.auth.login}>
|
||||
<Stack direction="row" align="center" justify="center" gap={2} group>
|
||||
<Icon icon={ArrowLeft} size={3.5} color="var(--color-primary)" groupHoverScale />
|
||||
<Group direction="row" align="center" justify="center" gap={2}>
|
||||
<Icon icon={ArrowLeft} size={3.5} color="var(--color-primary)" />
|
||||
<Text size="sm" weight="bold" color="text-primary-accent">Back to Login</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Link>
|
||||
</Box>
|
||||
</Group>
|
||||
</AuthForm>
|
||||
) : (
|
||||
<Stack gap={6}>
|
||||
<Box p={4} bg="success-green/10" border borderColor="success-green/30" rounded="md">
|
||||
<Stack direction="row" align="start" gap={3}>
|
||||
<Icon icon={CheckCircle2} size={5} color="var(--color-success)" />
|
||||
<Box>
|
||||
<Text size="sm" color="text-success-green" weight="bold" block>Password Reset</Text>
|
||||
<Text size="xs" color="text-gray-400" block mt={1}>{viewData.successMessage}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Group direction="column" gap={6} fullWidth>
|
||||
<Group direction="row" align="start" gap={3} fullWidth>
|
||||
<Icon icon={CheckCircle2} size={5} color="var(--color-success)" />
|
||||
<Group direction="column" gap={1}>
|
||||
<Text size="sm" color="text-success-green" weight="bold" block>Password Reset</Text>
|
||||
<Text size="xs" color="text-gray-400" block>{viewData.successMessage}</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
@@ -128,7 +123,7 @@ export function ResetPasswordTemplate({
|
||||
>
|
||||
Return to Login
|
||||
</Button>
|
||||
</Stack>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
<AuthFooterLinks>
|
||||
|
||||
@@ -5,15 +5,16 @@ import { AuthFooterLinks } from '@/components/auth/AuthFooterLinks';
|
||||
import { AuthForm } from '@/components/auth/AuthForm';
|
||||
import { SignupViewData } from '@/lib/builders/view-data/types/SignupViewData';
|
||||
import { checkPasswordStrength } from '@/lib/utils/validation';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { Group } from '@/ui/Group';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Input } from '@/ui/Input';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { LoadingSpinner } from '@/ui/LoadingSpinner';
|
||||
import { PasswordField } from '@/ui/PasswordField';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { ProgressBar } from '@/ui/ProgressBar';
|
||||
import { AlertCircle, Check, Mail, User, UserPlus, X } from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
@@ -47,16 +48,22 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
{ met: /[^a-zA-Z\d]/.test(passwordValue), label: 'Special' },
|
||||
];
|
||||
|
||||
const getStrengthIntent = () => {
|
||||
if (passwordStrength.score <= 2) return 'critical';
|
||||
if (passwordStrength.score <= 4) return 'warning';
|
||||
return 'success';
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthCard
|
||||
title="Create Account"
|
||||
description="Join the GridPilot racing community"
|
||||
>
|
||||
<AuthForm onSubmit={formActions.handleSubmit}>
|
||||
<Stack gap={6}>
|
||||
<Stack gap={4}>
|
||||
<Group direction="column" gap={6} fullWidth>
|
||||
<Group direction="column" gap={4} fullWidth>
|
||||
<Text size="xs" weight="bold" color="text-low" uppercase letterSpacing="wide" block>Personal Information</Text>
|
||||
<Box display="grid" gridCols={{ base: 1, md: 2 }} gap={4}>
|
||||
<Grid cols={{ base: 1, md: 2 }} gap={4}>
|
||||
<Input
|
||||
label="First Name"
|
||||
id="firstName"
|
||||
@@ -81,16 +88,14 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
autoComplete="family-name"
|
||||
icon={<User size={16} />}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Box p={3} bg="warning-amber/5" border borderColor="warning-amber/20" rounded="md">
|
||||
<Stack direction="row" align="start" gap={2}>
|
||||
<Icon icon={AlertCircle} size={3.5} color="var(--color-warning)" mt={0.5} />
|
||||
<Text size="xs" color="text-med">
|
||||
<Text weight="bold" color="text-warning-amber">Note:</Text> Your name cannot be changed after signup.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Group direction="row" align="start" gap={2} fullWidth>
|
||||
<Icon icon={AlertCircle} size={3.5} color="var(--color-warning)" />
|
||||
<Text size="xs" color="text-med">
|
||||
<Text weight="bold" color="text-warning-amber">Note:</Text> Your name cannot be changed after signup.
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<Input
|
||||
label="Email Address"
|
||||
@@ -105,9 +110,9 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
autoComplete="email"
|
||||
icon={<Mail size={16} />}
|
||||
/>
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
<Stack gap={4}>
|
||||
<Group direction="column" gap={4} fullWidth>
|
||||
<Text size="xs" weight="bold" color="text-low" uppercase letterSpacing="wide" block>Security</Text>
|
||||
<PasswordField
|
||||
label="Password"
|
||||
@@ -124,34 +129,30 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
/>
|
||||
|
||||
{passwordValue && (
|
||||
<Stack gap={3}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Box flexGrow={1} h="1px" bg="outline-steel" rounded="full" overflow="hidden">
|
||||
<Box
|
||||
h="full"
|
||||
transition
|
||||
w={`${(passwordStrength.score / 5) * 100}%`}
|
||||
bg={
|
||||
passwordStrength.score <= 2 ? 'critical-red' :
|
||||
passwordStrength.score <= 4 ? 'warning-amber' : 'success-green'
|
||||
}
|
||||
<Group direction="column" gap={3} fullWidth>
|
||||
<Group direction="row" align="center" gap={2} fullWidth>
|
||||
<Group fullWidth>
|
||||
<ProgressBar
|
||||
value={(passwordStrength.score / 5) * 100}
|
||||
intent={getStrengthIntent()}
|
||||
size="sm"
|
||||
/>
|
||||
</Box>
|
||||
</Group>
|
||||
<Text size="xs" weight="bold" color="text-low" uppercase>
|
||||
{passwordStrength.label}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Box display="grid" gridCols={2} gap={2}>
|
||||
</Group>
|
||||
<Grid cols={2} gap={2}>
|
||||
{passwordRequirements.map((req, index) => (
|
||||
<Stack key={index} direction="row" align="center" gap={1.5}>
|
||||
<Group key={index} direction="row" align="center" gap={1.5}>
|
||||
<Icon icon={req.met ? Check : X} size={3} color={req.met ? 'var(--color-success)' : 'var(--color-text-low)'} />
|
||||
<Text size="xs" color={req.met ? 'text-med' : 'text-low'}>
|
||||
{req.label}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
))}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
<PasswordField
|
||||
@@ -167,16 +168,14 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
showPassword={uiState.showConfirmPassword}
|
||||
onTogglePassword={() => formActions.setShowConfirmPassword(!uiState.showConfirmPassword)}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{mutationState.error && (
|
||||
<Box p={4} bg="critical-red/10" border borderColor="critical-red/30" rounded="md">
|
||||
<Stack direction="row" align="start" gap={3}>
|
||||
<Icon icon={AlertCircle} size={4.5} color="var(--color-critical)" />
|
||||
<Text size="sm" color="text-critical-red">{mutationState.error}</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Group direction="row" align="start" gap={3} fullWidth>
|
||||
<Icon icon={AlertCircle} size={4.5} color="var(--color-critical)" />
|
||||
<Text size="sm" color="text-critical-red">{mutationState.error}</Text>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
<Button
|
||||
@@ -200,14 +199,14 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
</Link>
|
||||
</Text>
|
||||
|
||||
<Box mt={2}>
|
||||
<Group direction="column" gap={1} align="center" fullWidth>
|
||||
<Text size="xs" color="text-gray-600">
|
||||
By creating an account, you agree to our{' '}
|
||||
<Link href="/terms">Terms</Link>
|
||||
{' '}and{' '}
|
||||
<Link href="/privacy">Privacy</Link>
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
</AuthFooterLinks>
|
||||
</AuthCard>
|
||||
);
|
||||
|
||||
@@ -1,84 +1,77 @@
|
||||
import { AppFooter } from '@/components/app/AppFooter';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { BrandMark } from '@/ui/BrandMark';
|
||||
import { Box } from '@/ui/Box';
|
||||
|
||||
export interface GlobalFooterViewData {}
|
||||
|
||||
export function GlobalFooterTemplate(_props: GlobalFooterViewData) {
|
||||
return (
|
||||
<AppFooter>
|
||||
<Box maxWidth="7xl" mx="auto" display="grid" responsiveGridCols={{ base: 1, md: 4 }} gap={12}>
|
||||
<Box colSpan={{ base: 1, md: 2 }}>
|
||||
<Box mb={6} opacity={0.8}>
|
||||
<Image
|
||||
src="/images/logos/wordmark-rectangle-dark.svg"
|
||||
alt="GridPilot"
|
||||
width={140}
|
||||
height={26}
|
||||
/>
|
||||
</Box>
|
||||
<Box maxWidth="sm" mb={6}>
|
||||
<Text color="text-gray-500">
|
||||
<Grid cols={{ base: 1, md: 4 }} gap={12}>
|
||||
<Stack colSpan={{ base: 1, md: 2 }} gap={6}>
|
||||
<Stack direction="row" align="center" gap={4}>
|
||||
<BrandMark />
|
||||
<Box display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={2} borderLeft borderColor="[#23272B]" pl={4}>
|
||||
<Box w="4px" h="4px" rounded="full" bg="primary-accent" animate="pulse" />
|
||||
<Text size="xs" variant="low" weight="bold" font="mono" letterSpacing="0.1em">
|
||||
INFRASTRUCTURE
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack maxWidth="sm">
|
||||
<Text variant="low" size="sm">
|
||||
The professional infrastructure for serious sim racing.
|
||||
Precision telemetry, automated results, and elite league management.
|
||||
</Text>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" gap={4}>
|
||||
<Text size="xs" color="text-gray-600" font="mono" letterSpacing="widest">
|
||||
© 2026 GRIDPILOT
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Box>
|
||||
<Box mb={4}>
|
||||
<Text weight="bold" color="text-gray-300" letterSpacing="wider">PLATFORM</Text>
|
||||
</Box>
|
||||
<Stack gap={4}>
|
||||
<Text size="xs" weight="bold" variant="high" uppercase letterSpacing="wider">PLATFORM</Text>
|
||||
<Stack as="ul" direction="col" gap={2}>
|
||||
<Box as="li">
|
||||
<Box as={Link} href="/leagues" color="text-gray-500" hoverTextColor="primary-accent" transition>
|
||||
<Stack as="li">
|
||||
<Link href="/leagues" variant="secondary">
|
||||
Leagues
|
||||
</Box>
|
||||
</Box>
|
||||
<Box as="li">
|
||||
<Box as={Link} href="/teams" color="text-gray-500" hoverTextColor="primary-accent" transition>
|
||||
</Link>
|
||||
</Stack>
|
||||
<Stack as="li">
|
||||
<Link href="/teams" variant="secondary">
|
||||
Teams
|
||||
</Box>
|
||||
</Box>
|
||||
<Box as="li">
|
||||
<Box as={Link} href="/leaderboards" color="text-gray-500" hoverTextColor="primary-accent" transition>
|
||||
</Link>
|
||||
</Stack>
|
||||
<Stack as="li">
|
||||
<Link href="/leaderboards" variant="secondary">
|
||||
Leaderboards
|
||||
</Box>
|
||||
</Box>
|
||||
</Link>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Box>
|
||||
<Box mb={4}>
|
||||
<Text weight="bold" color="text-gray-300" letterSpacing="wider">SUPPORT</Text>
|
||||
</Box>
|
||||
<Stack gap={4}>
|
||||
<Text size="xs" weight="bold" variant="high" uppercase letterSpacing="wider">SUPPORT</Text>
|
||||
<Stack as="ul" direction="col" gap={2}>
|
||||
<Box as="li">
|
||||
<Box as={Link} href="/docs" color="text-gray-500" hoverTextColor="primary-accent" transition>
|
||||
<Stack as="li">
|
||||
<Link href="/docs" variant="secondary">
|
||||
Documentation
|
||||
</Box>
|
||||
</Box>
|
||||
<Box as="li">
|
||||
<Box as={Link} href="/status" color="text-gray-500" hoverTextColor="primary-accent" transition>
|
||||
</Link>
|
||||
</Stack>
|
||||
<Stack as="li">
|
||||
<Link href="/status" variant="secondary">
|
||||
System Status
|
||||
</Box>
|
||||
</Box>
|
||||
<Box as="li">
|
||||
<Box as={Link} href="/contact" color="text-gray-500" hoverTextColor="primary-accent" transition>
|
||||
</Link>
|
||||
</Stack>
|
||||
<Stack as="li">
|
||||
<Link href="/contact" variant="secondary">
|
||||
Contact
|
||||
</Box>
|
||||
</Box>
|
||||
</Link>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</AppFooter>
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user