website refactor

This commit is contained in:
2026-01-15 18:52:03 +01:00
parent f035cfe7ce
commit 5ef149b782
39 changed files with 564 additions and 518 deletions

View File

@@ -122,7 +122,9 @@ export default function RacesAllPage() {
retry={fetchData} retry={fetchData}
Template={({ data: _data }) => ( Template={({ data: _data }) => (
<RacesAllTemplate <RacesAllTemplate
races={paginatedRaces} viewData={pageData}
races={paginatedRaces as any}
totalFilteredCount={filteredRaces.length}
isLoading={false} isLoading={false}
currentPage={currentPage} currentPage={currentPage}
totalPages={totalPages} totalPages={totalPages}

View File

@@ -0,0 +1,55 @@
'use client';
import React, { useState, useMemo } from 'react';
import { SponsorLeaguesTemplate, type SortOption, type TierFilter, type AvailabilityFilter } from '@/templates/SponsorLeaguesTemplate';
export default function SponsorLeaguesPageClient({ data }: { data: any }) {
const [searchQuery, setSearchQuery] = useState('');
const [tierFilter, setTierFilter] = useState<TierFilter>('all');
const [availabilityFilter, setAvailabilityFilter] = useState<AvailabilityFilter>('all');
const [sortBy, setSortBy] = useState<SortOption>('rating');
const filteredLeagues = useMemo(() => {
if (!data?.leagues) return [];
return data.leagues
.filter((league: any) => {
if (searchQuery && !league.name.toLowerCase().includes(searchQuery.toLowerCase())) {
return false;
}
if (tierFilter !== 'all' && league.tier !== tierFilter) {
return false;
}
if (availabilityFilter === 'main' && !league.mainSponsorSlot.available) {
return false;
}
if (availabilityFilter === 'secondary' && league.secondarySlots.available === 0) {
return false;
}
return true;
})
.sort((a: any, b: any) => {
switch (sortBy) {
case 'rating': return b.rating - a.rating;
case 'drivers': return b.drivers - a.drivers;
case 'price': return a.mainSponsorSlot.price - b.mainSponsorSlot.price;
case 'views': return b.avgViewsPerRace - a.avgViewsPerRace;
default: return 0;
}
});
}, [data?.leagues, searchQuery, tierFilter, availabilityFilter, sortBy]);
return (
<SponsorLeaguesTemplate
viewData={data}
filteredLeagues={filteredLeagues}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
tierFilter={tierFilter}
setTierFilter={setTierFilter}
availabilityFilter={availabilityFilter}
setAvailabilityFilter={setAvailabilityFilter}
sortBy={sortBy}
setSortBy={setSortBy}
/>
);
}

View File

@@ -0,0 +1,19 @@
'use client';
import React, { useState } from 'react';
import { SponsorLeagueDetailTemplate } from '@/templates/SponsorLeagueDetailTemplate';
export default function SponsorLeagueDetailPageClient({ data }: { data: any }) {
const [activeTab, setActiveTab] = useState<'overview' | 'drivers' | 'races' | 'sponsor'>('overview');
const [selectedTier, setSelectedTier] = useState<'main' | 'secondary'>('main');
return (
<SponsorLeagueDetailTemplate
viewData={data}
activeTab={activeTab}
setActiveTab={setActiveTab}
selectedTier={selectedTier}
setSelectedTier={setSelectedTier}
/>
);
}

View File

@@ -1,6 +1,6 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { PageWrapper } from '@/components/shared/state/PageWrapper'; import { PageWrapper } from '@/components/shared/state/PageWrapper';
import { SponsorLeagueDetailTemplate } from '@/templates/SponsorLeagueDetailTemplate'; import SponsorLeagueDetailPageClient from './SponsorLeagueDetailPageClient';
import { SponsorsApiClient } from '@/lib/api/sponsors/SponsorsApiClient'; import { SponsorsApiClient } from '@/lib/api/sponsors/SponsorsApiClient';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter'; import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger'; import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
@@ -25,5 +25,5 @@ export default async function Page({ params }: { params: { id: string } }) {
if (!data) notFound(); if (!data) notFound();
// Data is already in the right format from API client // Data is already in the right format from API client
return <PageWrapper data={data} Template={SponsorLeagueDetailTemplate} />; return <PageWrapper data={data} Template={SponsorLeagueDetailPageClient} />;
} }

View File

@@ -1,5 +1,5 @@
import { PageWrapper } from '@/components/shared/state/PageWrapper'; import { PageWrapper } from '@/components/shared/state/PageWrapper';
import { SponsorLeaguesTemplate } from '@/templates/SponsorLeaguesTemplate'; import SponsorLeaguesPageClient from './SponsorLeaguesPageClient';
import { SponsorsApiClient } from '@/lib/api/sponsors/SponsorsApiClient'; import { SponsorsApiClient } from '@/lib/api/sponsors/SponsorsApiClient';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter'; import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger'; import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
@@ -23,7 +23,7 @@ export default async function Page() {
// Process data - move business logic to template // Process data - move business logic to template
if (!leaguesData) { if (!leaguesData) {
return <PageWrapper data={undefined} Template={SponsorLeaguesTemplate} />; return <PageWrapper data={undefined} Template={SponsorLeaguesPageClient} />;
} }
// Calculate summary stats (business logic moved from view model) // Calculate summary stats (business logic moved from view model)
@@ -42,5 +42,5 @@ export default async function Page() {
stats, stats,
}; };
return <PageWrapper data={processedData} Template={SponsorLeaguesTemplate} />; return <PageWrapper data={processedData} Template={SponsorLeaguesPageClient} />;
} }

View File

@@ -16,4 +16,5 @@ export interface RulebookScoringConfig {
export interface LeagueRulebookViewData { export interface LeagueRulebookViewData {
scoringConfig: RulebookScoringConfig | null; scoringConfig: RulebookScoringConfig | null;
positionPoints: Array<{ position: number; points: number }>;
} }

View File

@@ -0,0 +1,12 @@
import type { TeamSummaryViewModel } from '../view-models/TeamSummaryViewModel';
export type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
export type SortBy = 'rating' | 'wins' | 'winRate' | 'races';
export interface TeamLeaderboardViewData {
teams: TeamSummaryViewModel[];
searchQuery: string;
filterLevel: SkillLevel | 'all';
sortBy: SortBy;
filteredAndSortedTeams: TeamSummaryViewModel[];
}

View File

@@ -1,5 +1,7 @@
export interface LeagueSponsorshipsViewData { export interface LeagueSponsorshipsViewData {
leagueId: string; leagueId: string;
activeTab: 'overview' | 'editor';
onTabChange: (tab: 'overview' | 'editor') => void;
league: { league: {
id: string; id: string;
name: string; name: string;

View File

@@ -53,7 +53,7 @@ export function AdminDashboardTemplate({
onClick={onRefresh} onClick={onRefresh}
disabled={isLoading} disabled={isLoading}
variant="secondary" variant="secondary"
icon={<Icon icon={RefreshCw} size={4} className={isLoading ? 'animate-spin' : ''} />} icon={<Icon icon={RefreshCw} size={4} animate={isLoading ? 'spin' : 'none'} />}
> >
Refresh Refresh
</Button> </Button>

View File

@@ -65,11 +65,11 @@ export function AdminUsersTemplate({
} }
}; };
const getRoleBadgeStyle = (role: string) => { const getRoleBadgeProps = (role: string): { bg: string; color: string; borderColor: string } => {
switch (role) { switch (role) {
case 'owner': return { backgroundColor: 'rgba(168, 85, 247, 0.2)', color: '#d8b4fe', border: '1px solid rgba(168, 85, 247, 0.3)' }; case 'owner': return { bg: 'bg-purple-500/20', color: '#d8b4fe', borderColor: 'border-purple-500/30' };
case 'admin': return { backgroundColor: 'rgba(59, 130, 246, 0.2)', color: '#93c5fd', border: '1px solid rgba(59, 130, 246, 0.3)' }; case 'admin': return { bg: 'bg-blue-500/20', color: '#93c5fd', borderColor: 'border-blue-500/30' };
default: return { backgroundColor: 'rgba(115, 115, 115, 0.2)', color: '#d1d5db', border: '1px solid rgba(115, 115, 115, 0.3)' }; default: return { bg: 'bg-neutral-500/20', color: '#d1d5db', borderColor: 'border-neutral-500/30' };
} }
}; };
@@ -86,7 +86,7 @@ export function AdminUsersTemplate({
onClick={onRefresh} onClick={onRefresh}
disabled={loading} disabled={loading}
variant="secondary" variant="secondary"
icon={<Icon icon={RefreshCw} size={4} className={loading ? 'animate-spin' : ''} />} icon={<Icon icon={RefreshCw} size={4} animate={loading ? 'spin' : 'none'} />}
> >
Refresh Refresh
</Button> </Button>
@@ -117,7 +117,7 @@ export function AdminUsersTemplate({
<Card p={0}> <Card p={0}>
{loading ? ( {loading ? (
<Stack center py={12} gap={3}> <Stack center py={12} gap={3}>
<Box className="animate-spin" style={{ borderRadius: '9999px', height: '2rem', width: '2rem', borderBottom: '2px solid #3b82f6' }} /> <Box animate="spin" rounded="full" h="2rem" w="2rem" borderBottom borderColor="border-primary-blue" />
<Text color="text-gray-400">Loading users...</Text> <Text color="text-gray-400">Loading users...</Text>
</Stack> </Stack>
) : !viewData.users || viewData.users.length === 0 ? ( ) : !viewData.users || viewData.users.length === 0 ? (
@@ -149,7 +149,7 @@ export function AdminUsersTemplate({
<TableRow key={user.id}> <TableRow key={user.id}>
<TableCell> <TableCell>
<Stack direction="row" align="center" gap={3}> <Stack direction="row" align="center" gap={3}>
<Surface variant="muted" rounded="full" padding={2} style={{ backgroundColor: 'rgba(59, 130, 246, 0.2)' }}> <Surface variant="muted" rounded="full" padding={2} bg="bg-primary-blue/20">
<Icon icon={Shield} size={4} color="#3b82f6" /> <Icon icon={Shield} size={4} color="#3b82f6" />
</Surface> </Surface>
<Box> <Box>
@@ -167,21 +167,18 @@ export function AdminUsersTemplate({
<TableCell> <TableCell>
<Stack direction="row" gap={1} wrap> <Stack direction="row" gap={1} wrap>
{user.roles.map((role, idx) => { {user.roles.map((role, idx) => {
const style = getRoleBadgeStyle(role); const badgeProps = getRoleBadgeProps(role);
return ( return (
<Surface <Surface
key={idx} key={idx}
variant="muted" variant="muted"
rounded="full" rounded="full"
padding={1} padding={1}
style={{ px={2}
paddingLeft: '0.5rem', bg={badgeProps.bg}
paddingRight: '0.5rem', color={badgeProps.color}
backgroundColor: style.backgroundColor, borderColor={badgeProps.borderColor}
color: style.color, border
borderColor: style.border,
border: '1px solid'
}}
> >
<Text size="xs" weight="medium">{role.charAt(0).toUpperCase() + role.slice(1)}</Text> <Text size="xs" weight="medium">{role.charAt(0).toUpperCase() + role.slice(1)}</Text>
</Surface> </Surface>

View File

@@ -43,7 +43,7 @@ export function DriverRankingsTemplate({
)} )}
<Stack direction="row" align="center" gap={4}> <Stack direction="row" align="center" gap={4}>
<Surface variant="muted" rounded="xl" padding={3} style={{ background: 'linear-gradient(to bottom right, rgba(59, 130, 246, 0.2), rgba(59, 130, 246, 0.05))', border: '1px solid rgba(59, 130, 246, 0.2)' }}> <Surface variant="muted" rounded="xl" padding={3} bg="linear-gradient(to bottom right, rgba(59, 130, 246, 0.2), rgba(59, 130, 246, 0.05))" border borderColor="border-blue-500/20">
<Icon icon={Trophy} size={7} color="#3b82f6" /> <Icon icon={Trophy} size={7} color="#3b82f6" />
</Surface> </Surface>
<Box> <Box>

View File

@@ -4,6 +4,8 @@ import React from 'react';
import { import {
Search, Search,
Crown, Crown,
Users,
Trophy,
} from 'lucide-react'; } from 'lucide-react';
import { Heading } from '@/ui/Heading'; import { Heading } from '@/ui/Heading';
import { Box } from '@/ui/Box'; import { Box } from '@/ui/Box';
@@ -20,7 +22,6 @@ import { CategoryDistribution } from '@/ui/CategoryDistribution';
import { LeaderboardPreview } from '@/ui/LeaderboardPreview'; import { LeaderboardPreview } from '@/ui/LeaderboardPreview';
import { RecentActivity } from '@/ui/RecentActivity'; import { RecentActivity } from '@/ui/RecentActivity';
import { PageHero } from '@/ui/PageHero'; import { PageHero } from '@/ui/PageHero';
import { Users, Trophy } from 'lucide-react';
import { DriversSearch } from '@/ui/DriversSearch'; import { DriversSearch } from '@/ui/DriversSearch';
import { EmptyState } from '@/ui/EmptyState'; import { EmptyState } from '@/ui/EmptyState';
import type { DriversViewData } from '@/lib/types/view-data/DriversViewData'; import type { DriversViewData } from '@/lib/types/view-data/DriversViewData';

View File

@@ -11,7 +11,6 @@ import { CareerProgressionMockup } from '@/components/mockups/CareerProgressionM
import { RaceHistoryMockup } from '@/components/mockups/RaceHistoryMockup'; import { RaceHistoryMockup } from '@/components/mockups/RaceHistoryMockup';
import { CompanionAutomationMockup } from '@/components/mockups/CompanionAutomationMockup'; import { CompanionAutomationMockup } from '@/components/mockups/CompanionAutomationMockup';
import { SimPlatformMockup } from '@/components/mockups/SimPlatformMockup'; import { SimPlatformMockup } from '@/components/mockups/SimPlatformMockup';
import { MockupStack } from '@/ui/MockupStack';
import { Card } from '@/ui/Card'; import { Card } from '@/ui/Card';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { Box } from '@/ui/Box'; import { Box } from '@/ui/Box';
@@ -65,7 +64,7 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
description={ description={
<Stack gap={4}> <Stack gap={4}>
<Text> <Text>
Your races, your seasons, your progress finally in one place. Your races, your seasons, your progress &mdash; finally in one place.
</Text> </Text>
<Stack gap={3}> <Stack gap={3}>
<FeatureItem text="Lifetime stats and season history across all your leagues" /> <FeatureItem text="Lifetime stats and season history across all your leagues" />
@@ -93,7 +92,7 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
Every race you run stays with you. Every race you run stays with you.
</Text> </Text>
<Stack gap={3}> <Stack gap={3}>
<ResultItem text="Your stats, your team, your story all connected" color="#ef4444" /> <ResultItem text="Your stats, your team, your story &mdash; all connected" color="#ef4444" />
<ResultItem text="One race result updates your profile, team points, rating, and season history" color="#ef4444" /> <ResultItem text="One race result updates your profile, team points, rating, and season history" color="#ef4444" />
<ResultItem text="No more fragmented data across spreadsheets and forums" color="#ef4444" /> <ResultItem text="No more fragmented data across spreadsheets and forums" color="#ef4444" />
</Stack> </Stack>
@@ -102,7 +101,7 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
</Text> </Text>
</Stack> </Stack>
} }
mockup={<MockupStack index={1}><RaceHistoryMockup /></MockupStack>} mockup={<RaceHistoryMockup />}
layout="text-right" layout="text-right"
/> />
@@ -112,7 +111,7 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
description={ description={
<Stack gap={4}> <Stack gap={4}>
<Text size="sm"> <Text size="sm">
Setting up league races used to mean clicking through iRacing's wizard 20 times. Setting up league races used to mean clicking through iRacing&apos;s wizard 20 times.
</Text> </Text>
<Stack gap={3}> <Stack gap={3}>
<StepItem step={1} text="Our companion app syncs with your league schedule" /> <StepItem step={1} text="Our companion app syncs with your league schedule" />
@@ -135,10 +134,10 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
description={ description={
<Stack gap={4}> <Stack gap={4}>
<Text size="sm"> <Text size="sm">
Right now, we're focused on making iRacing league racing better. Right now, we&apos;re focused on making iRacing league racing better.
</Text> </Text>
<Text size="sm"> <Text size="sm">
But sims come and go. Your leagues, your teams, your ratingthose stay. But sims come and go. Your leagues, your teams, your rating &mdash; those stay.
</Text> </Text>
<Text size="sm"> <Text size="sm">
GridPilot is built to outlast any single platform. GridPilot is built to outlast any single platform.
@@ -168,7 +167,7 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
<Card> <Card>
<Stack gap={4}> <Stack gap={4}>
<Stack direction="row" align="center" justify="between"> <Stack direction="row" align="center" justify="between">
<Heading level={3} style={{ fontSize: '0.875rem' }}>Featured leagues</Heading> <Heading level={3} fontSize="sm">Featured leagues</Heading>
<Link href={routes.public.leagues}> <Link href={routes.public.leagues}>
<Button variant="secondary" size="sm"> <Button variant="secondary" size="sm">
View all View all
@@ -179,12 +178,12 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
{viewData.topLeagues.slice(0, 4).map((league) => ( {viewData.topLeagues.slice(0, 4).map((league) => (
<Box key={league.id}> <Box key={league.id}>
<Stack direction="row" align="start" gap={3}> <Stack direction="row" align="start" gap={3}>
<Surface variant="muted" rounded="md" border padding={1} style={{ width: '2.5rem', height: '2.5rem', backgroundColor: 'rgba(59, 130, 246, 0.1)', borderColor: 'rgba(59, 130, 246, 0.3)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <Surface variant="muted" rounded="md" border padding={1} w="2.5rem" h="2.5rem" bg="bg-blue-500/10" borderColor="border-blue-500/30" display="flex" alignItems="center" justifyContent="center">
<Text size="xs" weight="bold" color="text-primary-blue"> <Text size="xs" weight="bold" color="text-primary-blue">
{league.name.split(' ').map((word) => word[0]).join('').slice(0, 3).toUpperCase()} {league.name.split(' ').map((word) => word[0]).join('').slice(0, 3).toUpperCase()}
</Text> </Text>
</Surface> </Surface>
<Box style={{ flex: 1, minWidth: 0 }}> <Box flex={1} minWidth="0">
<Text color="text-white" block truncate>{league.name}</Text> <Text color="text-white" block truncate>{league.name}</Text>
<Text size="xs" color="text-gray-400" block mt={1} truncate>{league.description}</Text> <Text size="xs" color="text-gray-400" block mt={1} truncate>{league.description}</Text>
</Box> </Box>
@@ -199,7 +198,7 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
<Card> <Card>
<Stack gap={4}> <Stack gap={4}>
<Stack direction="row" align="center" justify="between"> <Stack direction="row" align="center" justify="between">
<Heading level={3} style={{ fontSize: '0.875rem' }}>Teams on the grid</Heading> <Heading level={3} fontSize="sm">Teams on the grid</Heading>
<Link href={routes.public.teams}> <Link href={routes.public.teams}>
<Button variant="secondary" size="sm"> <Button variant="secondary" size="sm">
Browse teams Browse teams
@@ -210,16 +209,18 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
{viewData.teams.slice(0, 4).map(team => ( {viewData.teams.slice(0, 4).map(team => (
<Box key={team.id}> <Box key={team.id}>
<Stack direction="row" align="start" gap={3}> <Stack direction="row" align="start" gap={3}>
<Surface variant="muted" rounded="md" border padding={1} style={{ width: '2.5rem', height: '2.5rem', overflow: 'hidden', backgroundColor: '#262626' }}> <Surface variant="muted" rounded="md" border padding={1} w="2.5rem" h="2.5rem" overflow="hidden" bg="bg-neutral-800">
<Image <Image
src={team.logoUrl || getMediaUrl('team-logo', team.id)} src={team.logoUrl || getMediaUrl('team-logo', team.id)}
alt={team.name} alt={team.name}
width={40} width={40}
height={40} height={40}
style={{ width: '100%', height: '100%', objectFit: 'cover' }} objectFit="cover"
fullWidth
fullHeight
/> />
</Surface> </Surface>
<Box style={{ flex: 1, minWidth: 0 }}> <Box flex={1} minWidth="0">
<Text color="text-white" block truncate>{team.name}</Text> <Text color="text-white" block truncate>{team.name}</Text>
<Text size="xs" color="text-gray-400" block mt={1} truncate>{team.description}</Text> <Text size="xs" color="text-gray-400" block mt={1} truncate>{team.description}</Text>
</Box> </Box>
@@ -234,7 +235,7 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
<Card> <Card>
<Stack gap={4}> <Stack gap={4}>
<Stack direction="row" align="center" justify="between"> <Stack direction="row" align="center" justify="between">
<Heading level={3} style={{ fontSize: '0.875rem' }}>Upcoming races</Heading> <Heading level={3} fontSize="sm">Upcoming races</Heading>
<Link href={routes.public.races}> <Link href={routes.public.races}>
<Button variant="secondary" size="sm"> <Button variant="secondary" size="sm">
View schedule View schedule
@@ -250,11 +251,11 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
{viewData.upcomingRaces.map(race => ( {viewData.upcomingRaces.map(race => (
<Box key={race.id}> <Box key={race.id}>
<Stack direction="row" align="start" justify="between" gap={3}> <Stack direction="row" align="start" justify="between" gap={3}>
<Box style={{ flex: 1, minWidth: 0 }}> <Box flex={1} minWidth="0">
<Text color="text-white" block truncate>{race.track}</Text> <Text color="text-white" block truncate>{race.track}</Text>
<Text size="xs" color="text-gray-400" block mt={1} truncate>{race.car}</Text> <Text size="xs" color="text-gray-400" block mt={1} truncate>{race.car}</Text>
</Box> </Box>
<Text size="xs" color="text-gray-500" style={{ whiteSpace: 'nowrap' }}> <Text size="xs" color="text-gray-500">
{race.formattedDate} {race.formattedDate}
</Text> </Text>
</Stack> </Stack>

View File

@@ -44,7 +44,11 @@ export function LeaderboardsTemplate({
</GridItem> </GridItem>
<GridItem colSpan={12} lgSpan={6}> <GridItem colSpan={12} lgSpan={6}>
<TeamLeaderboardPreview <TeamLeaderboardPreview
topTeams={viewData.teams as any} topTeams={viewData.teams.map(team => ({
...team,
isRecruiting: false,
performanceLevel: 'N/A'
}))}
onTeamClick={onTeamClick} onTeamClick={onTeamClick}
onViewFullLeaderboard={onNavigateToTeams} onViewFullLeaderboard={onNavigateToTeams}
/> />

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { useMemo } from 'react'; import React from 'react';
import { Card } from '@/ui/Card'; import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box'; import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
@@ -10,7 +10,6 @@ import { Button } from '@/ui/Button';
import { Input } from '@/ui/Input'; import { Input } from '@/ui/Input';
import { Select } from '@/ui/Select'; import { Select } from '@/ui/Select';
import { Grid } from '@/ui/Grid'; import { Grid } from '@/ui/Grid';
import { GridItem } from '@/ui/GridItem';
import { Surface } from '@/ui/Surface'; import { Surface } from '@/ui/Surface';
import type { LeagueAdminScheduleViewData } from '@/lib/view-data/LeagueAdminScheduleViewData'; import type { LeagueAdminScheduleViewData } from '@/lib/view-data/LeagueAdminScheduleViewData';
@@ -64,10 +63,7 @@ export function LeagueAdminScheduleTemplate({
const isEditing = editingRaceId !== null; const isEditing = editingRaceId !== null;
const publishedLabel = published ? 'Published' : 'Unpublished'; const publishedLabel = published ? 'Published' : 'Unpublished';
const selectedSeasonLabel = useMemo(() => { const selectedSeasonLabel = seasons.find((s) => s.seasonId === seasonId)?.name ?? seasonId;
const selected = seasons.find((s) => s.seasonId === seasonId);
return selected?.name ?? seasonId;
}, [seasons, seasonId]);
return ( return (
<Stack gap={6}> <Stack gap={6}>
@@ -102,7 +98,7 @@ export function LeagueAdminScheduleTemplate({
</Button> </Button>
</Stack> </Stack>
<Box pt={6} style={{ borderTop: '1px solid #262626' }}> <Box pt={6} borderTop="1px solid" borderColor="border-neutral-800">
<Box mb={4}> <Box mb={4}>
<Heading level={2}>{isEditing ? 'Edit race' : 'Add race'}</Heading> <Heading level={2}>{isEditing ? 'Edit race' : 'Add race'}</Heading>
</Box> </Box>
@@ -156,7 +152,7 @@ export function LeagueAdminScheduleTemplate({
</Stack> </Stack>
</Box> </Box>
<Box pt={6} style={{ borderTop: '1px solid #262626' }}> <Box pt={6} borderTop="1px solid" borderColor="border-neutral-800">
<Box mb={4}> <Box mb={4}>
<Heading level={2}>Races</Heading> <Heading level={2}>Races</Heading>
</Box> </Box>

View File

@@ -50,10 +50,7 @@ export function LeagueRulebookTemplate({
const { scoringConfig } = viewData; const { scoringConfig } = viewData;
const primaryChampionship = scoringConfig.championships.find(c => c.type === 'driver') ?? scoringConfig.championships[0]; const primaryChampionship = scoringConfig.championships.find(c => c.type === 'driver') ?? scoringConfig.championships[0];
const positionPoints: { position: number; points: number }[] = primaryChampionship?.pointsPreview const positionPoints = viewData.positionPoints;
.filter((p) => p.sessionType === primaryChampionship.sessionTypes[0])
.map(p => ({ position: p.position, points: p.points }))
.sort((a, b) => a.position - b.position) || [];
return ( return (
<Stack gap={6}> <Stack gap={6}>
@@ -86,24 +83,24 @@ export function LeagueRulebookTemplate({
<Card> <Card>
<Stack gap={4}> <Stack gap={4}>
<Stack direction="row" align="center" gap={2}> <Stack direction="row" align="center" gap={2}>
<Clock className="w-5 h-5 text-primary-blue" /> <Clock size={20} color="#3b82f6" />
<Heading level={2}>Weekend Structure & Timings</Heading> <Heading level={2}>Weekend Structure & Timings</Heading>
</Stack> </Stack>
<Grid cols={4} gap={4}> <Grid cols={4} gap={4}>
<Surface variant="muted" rounded="lg" border padding={3}> <Surface variant="muted" rounded="lg" border padding={3}>
<Text size="xs" color="text-gray-500" block mb={1} style={{ textTransform: 'uppercase' }}>Practice</Text> <Text size="xs" color="text-gray-500" block mb={1}>PRACTICE</Text>
<Text weight="medium" color="text-white">20 min</Text> <Text weight="medium" color="text-white">20 min</Text>
</Surface> </Surface>
<Surface variant="muted" rounded="lg" border padding={3}> <Surface variant="muted" rounded="lg" border padding={3}>
<Text size="xs" color="text-gray-500" block mb={1} style={{ textTransform: 'uppercase' }}>Qualifying</Text> <Text size="xs" color="text-gray-500" block mb={1}>QUALIFYING</Text>
<Text weight="medium" color="text-white">30 min</Text> <Text weight="medium" color="text-white">30 min</Text>
</Surface> </Surface>
<Surface variant="muted" rounded="lg" border padding={3}> <Surface variant="muted" rounded="lg" border padding={3}>
<Text size="xs" color="text-gray-500" block mb={1} style={{ textTransform: 'uppercase' }}>Sprint</Text> <Text size="xs" color="text-gray-500" block mb={1}>SPRINT</Text>
<Text weight="medium" color="text-white"></Text> <Text weight="medium" color="text-white"></Text>
</Surface> </Surface>
<Surface variant="muted" rounded="lg" border padding={3}> <Surface variant="muted" rounded="lg" border padding={3}>
<Text size="xs" color="text-gray-500" block mb={1} style={{ textTransform: 'uppercase' }}>Main Race</Text> <Text size="xs" color="text-gray-500" block mb={1}>MAIN RACE</Text>
<Text weight="medium" color="text-white">40 min</Text> <Text weight="medium" color="text-white">40 min</Text>
</Surface> </Surface>
</Grid> </Grid>
@@ -128,7 +125,7 @@ export function LeagueRulebookTemplate({
padding={3} padding={3}
> >
<Stack direction="row" align="center" gap={4}> <Stack direction="row" align="center" gap={4}>
<Surface variant="muted" rounded="full" padding={1} style={{ width: '2rem', height: '2rem', backgroundColor: 'rgba(16, 185, 129, 0.1)', border: '1px solid rgba(16, 185, 129, 0.2)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <Surface variant="muted" rounded="full" padding={1} w="2rem" h="2rem" bg="bg-green-500/10" borderColor="border-green-500/20" display="flex" alignItems="center" justifyContent="center">
<Text color="text-performance-green" weight="bold">+</Text> <Text color="text-performance-green" weight="bold">+</Text>
</Surface> </Surface>
<Text size="sm" color="text-gray-300">{bonus}</Text> <Text size="sm" color="text-gray-300">{bonus}</Text>
@@ -222,9 +219,9 @@ export function LeagueRulebookTemplate({
function StatItem({ label, value }: { label: string, value: string | number }) { function StatItem({ label, value }: { label: string, value: string | number }) {
return ( return (
<Surface variant="muted" rounded="lg" border padding={4} style={{ backgroundColor: '#262626', borderColor: '#262626' }}> <Surface variant="muted" rounded="lg" border padding={4} bg="bg-neutral-800" borderColor="border-neutral-800">
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }} block mb={1}>{label}</Text> <Text size="xs" color="text-gray-500" block mb={1}>{label.toUpperCase()}</Text>
<Text weight="semibold" color="text-white" style={{ fontSize: '1.125rem' }}>{value}</Text> <Text weight="semibold" color="text-white" size="lg">{value}</Text>
</Surface> </Surface>
); );
} }
@@ -245,7 +242,7 @@ function PenaltyRow({ infraction, penalty, color }: { infraction: string, penalt
<Text size="sm" color="text-gray-300">{infraction}</Text> <Text size="sm" color="text-gray-300">{infraction}</Text>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Text size="sm" style={{ color: color || '#f59e0b' }}>{penalty}</Text> <Text size="sm" color={color === '#f87171' ? 'text-error-red' : 'text-warning-amber'}>{penalty}</Text>
</TableCell> </TableCell>
</TableRow> </TableRow>
); );

View File

@@ -10,7 +10,7 @@ import { Grid } from '@/ui/Grid';
import { GridItem } from '@/ui/GridItem'; import { GridItem } from '@/ui/GridItem';
import { Icon } from '@/ui/Icon'; import { Icon } from '@/ui/Icon';
import { Surface } from '@/ui/Surface'; import { Surface } from '@/ui/Surface';
import { Settings, Users, Trophy, Shield, Clock } from 'lucide-react'; import { Settings, Users, Trophy, Shield, Clock, LucideIcon } from 'lucide-react';
import type { LeagueSettingsViewData } from '@/lib/view-data/leagues/LeagueSettingsViewData'; import type { LeagueSettingsViewData } from '@/lib/view-data/leagues/LeagueSettingsViewData';
interface LeagueSettingsTemplateProps { interface LeagueSettingsTemplateProps {
@@ -32,7 +32,7 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
<Card> <Card>
<Stack gap={4}> <Stack gap={4}>
<Stack direction="row" align="center" gap={3}> <Stack direction="row" align="center" gap={3}>
<Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: 'rgba(59, 130, 246, 0.1)' }}> <Surface variant="muted" rounded="lg" padding={2} bg="bg-blue-500/10">
<Icon icon={Settings} size={5} color="#3b82f6" /> <Icon icon={Settings} size={5} color="#3b82f6" />
</Surface> </Surface>
<Box> <Box>
@@ -48,7 +48,7 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
<InfoItem label="Description" value={viewData.league.description} /> <InfoItem label="Description" value={viewData.league.description} />
</GridItem> </GridItem>
<InfoItem label="Created" value={new Date(viewData.league.createdAt).toLocaleDateString()} /> <InfoItem label="Created" value={new Date(viewData.league.createdAt).toLocaleDateString()} />
<InfoItem label="Owner ID" value={viewData.league.ownerId} mono /> <InfoItem label="Owner ID" value={viewData.league.ownerId} />
</Grid> </Grid>
</Stack> </Stack>
</Card> </Card>
@@ -57,7 +57,7 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
<Card> <Card>
<Stack gap={4}> <Stack gap={4}>
<Stack direction="row" align="center" gap={3}> <Stack direction="row" align="center" gap={3}>
<Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: 'rgba(16, 185, 129, 0.1)' }}> <Surface variant="muted" rounded="lg" padding={2} bg="bg-green-500/10">
<Icon icon={Trophy} size={5} color="#10b981" /> <Icon icon={Trophy} size={5} color="#10b981" />
</Surface> </Surface>
<Box> <Box>
@@ -70,7 +70,7 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
<ConfigItem icon={Users} label="Max Drivers" value={viewData.config.maxDrivers} /> <ConfigItem icon={Users} label="Max Drivers" value={viewData.config.maxDrivers} />
<ConfigItem icon={Shield} label="Require Approval" value={viewData.config.requireApproval ? 'Yes' : 'No'} /> <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'} /> <ConfigItem icon={Clock} label="Allow Late Join" value={viewData.config.allowLateJoin ? 'Yes' : 'No'} />
<ConfigItem icon={Trophy} label="Scoring Preset" value={viewData.config.scoringPresetId} mono /> <ConfigItem icon={Trophy} label="Scoring Preset" value={viewData.config.scoringPresetId} />
</Grid> </Grid>
</Stack> </Stack>
</Card> </Card>
@@ -78,10 +78,10 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
{/* Note about forms */} {/* Note about forms */}
<Card> <Card>
<Stack align="center" py={8} gap={4}> <Stack align="center" py={8} gap={4}>
<Surface variant="muted" rounded="full" padding={4} style={{ backgroundColor: 'rgba(245, 158, 11, 0.1)' }}> <Surface variant="muted" rounded="full" padding={4} bg="bg-amber-500/10">
<Icon icon={Settings} size={8} color="#f59e0b" /> <Icon icon={Settings} size={8} color="#f59e0b" />
</Surface> </Surface>
<Box style={{ textAlign: 'center' }}> <Box textAlign="center">
<Heading level={3}>Settings Management</Heading> <Heading level={3}>Settings Management</Heading>
<Text size="sm" color="text-gray-400" block mt={2}> <Text size="sm" color="text-gray-400" block mt={2}>
Form-based editing and ownership transfer functionality will be implemented in future updates. Form-based editing and ownership transfer functionality will be implemented in future updates.
@@ -94,22 +94,22 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
); );
} }
function InfoItem({ label, value, capitalize, mono }: { label: string, value: string, capitalize?: boolean, mono?: boolean }) { function InfoItem({ label, value, capitalize }: { label: string, value: string, capitalize?: boolean }) {
return ( return (
<Box> <Box>
<Text size="sm" weight="medium" color="text-gray-400" block mb={1}>{label}</Text> <Text size="sm" weight="medium" color="text-gray-400" block mb={1}>{label}</Text>
<Text color="text-white" style={{ textTransform: capitalize ? 'capitalize' : 'none', fontFamily: mono ? 'monospace' : 'inherit' }}>{value}</Text> <Text color="text-white">{capitalize ? value.toUpperCase() : value}</Text>
</Box> </Box>
); );
} }
function ConfigItem({ icon, label, value, mono }: { icon: React.ElementType, label: string, value: string | number, mono?: boolean }) { function ConfigItem({ icon, label, value }: { icon: LucideIcon, label: string, value: string | number }) {
return ( return (
<Stack direction="row" align="center" gap={3}> <Stack direction="row" align="center" gap={3}>
<Icon icon={icon as any} size={5} color="#9ca3af" /> <Icon icon={icon} size={5} color="#9ca3af" />
<Box> <Box>
<Text size="sm" weight="medium" color="text-gray-400" block>{label}</Text> <Text size="sm" weight="medium" color="text-gray-400" block>{label}</Text>
<Text color="text-white" style={{ fontFamily: mono ? 'monospace' : 'inherit' }}>{value}</Text> <Text color="text-white">{value}</Text>
</Box> </Box>
</Stack> </Stack>
); );

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React from 'react';
import { Card } from '@/ui/Card'; import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box'; import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
@@ -20,7 +20,7 @@ interface LeagueSponsorshipsTemplateProps {
} }
export function LeagueSponsorshipsTemplate({ viewData }: LeagueSponsorshipsTemplateProps) { export function LeagueSponsorshipsTemplate({ viewData }: LeagueSponsorshipsTemplateProps) {
const [activeTab, setActiveTab] = useState<'overview' | 'editor'>('overview'); const activeTab = viewData.activeTab;
return ( return (
<Stack gap={6}> <Stack gap={6}>
@@ -32,26 +32,36 @@ export function LeagueSponsorshipsTemplate({ viewData }: LeagueSponsorshipsTempl
</Text> </Text>
</Box> </Box>
<Stack direction="row" gap={2}> <Stack direction="row" gap={2}>
<button <Box
onClick={() => setActiveTab('overview')} as="button"
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${ onClick={() => viewData.onTabChange('overview')}
activeTab === 'overview' px={4}
? 'bg-primary-blue text-white' py={2}
: 'bg-iron-gray text-gray-400 hover:text-white' rounded="lg"
}`} size="sm"
weight="medium"
bg={activeTab === 'overview' ? 'bg-primary-blue' : 'bg-iron-gray'}
color={activeTab === 'overview' ? 'text-white' : 'text-gray-400'}
cursor="pointer"
borderStyle="none"
> >
Overview Overview
</button> </Box>
<button <Box
onClick={() => setActiveTab('editor')} as="button"
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${ onClick={() => viewData.onTabChange('editor')}
activeTab === 'editor' px={4}
? 'bg-primary-blue text-white' py={2}
: 'bg-iron-gray text-gray-400 hover:text-white' rounded="lg"
}`} size="sm"
weight="medium"
bg={activeTab === 'editor' ? 'bg-primary-blue' : 'bg-iron-gray'}
color={activeTab === 'editor' ? 'text-white' : 'text-gray-400'}
cursor="pointer"
borderStyle="none"
> >
Livery Editor Livery Editor
</button> </Box>
</Stack> </Stack>
</Stack> </Stack>
@@ -61,7 +71,7 @@ export function LeagueSponsorshipsTemplate({ viewData }: LeagueSponsorshipsTempl
<Card> <Card>
<Stack gap={4}> <Stack gap={4}>
<Stack direction="row" align="center" gap={3}> <Stack direction="row" align="center" gap={3}>
<Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: 'rgba(59, 130, 246, 0.1)' }}> <Surface variant="muted" rounded="lg" padding={2} bg="bg-blue-500/10">
<Icon icon={Building} size={5} color="#3b82f6" /> <Icon icon={Building} size={5} color="#3b82f6" />
</Surface> </Surface>
<Box> <Box>
@@ -89,7 +99,7 @@ export function LeagueSponsorshipsTemplate({ viewData }: LeagueSponsorshipsTempl
<Card> <Card>
<Stack gap={4}> <Stack gap={4}>
<Stack direction="row" align="center" gap={3}> <Stack direction="row" align="center" gap={3}>
<Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: 'rgba(245, 158, 11, 0.1)' }}> <Surface variant="muted" rounded="lg" padding={2} bg="bg-amber-500/10">
<Icon icon={Clock} size={5} color="#f59e0b" /> <Icon icon={Clock} size={5} color="#f59e0b" />
</Surface> </Surface>
<Box> <Box>
@@ -112,7 +122,6 @@ export function LeagueSponsorshipsTemplate({ viewData }: LeagueSponsorshipsTempl
key={request.id} key={request.id}
request={{ request={{
...request, ...request,
status: request.status as any,
slotName: slot?.name || 'Unknown slot' slotName: slot?.name || 'Unknown slot'
}} }}
/> />
@@ -127,7 +136,7 @@ export function LeagueSponsorshipsTemplate({ viewData }: LeagueSponsorshipsTempl
<Card> <Card>
<Stack gap={6}> <Stack gap={6}>
<Stack direction="row" align="center" gap={3}> <Stack direction="row" align="center" gap={3}>
<Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: 'rgba(168, 85, 247, 0.1)' }}> <Surface variant="muted" rounded="lg" padding={2} bg="bg-purple-500/10">
<Icon icon={Palette} size={5} color="#a855f7" /> <Icon icon={Palette} size={5} color="#a855f7" />
</Surface> </Surface>
<Box> <Box>

View File

@@ -4,7 +4,6 @@ import React from 'react';
import { LeagueChampionshipStats } from '@/components/leagues/LeagueChampionshipStats'; import { LeagueChampionshipStats } from '@/components/leagues/LeagueChampionshipStats';
import { StandingsTable } from '@/components/leagues/StandingsTable'; import { StandingsTable } from '@/components/leagues/StandingsTable';
import { Card } from '@/ui/Card'; import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading'; import { Heading } from '@/ui/Heading';

View File

@@ -31,7 +31,7 @@ export function LeagueWalletTemplate({ viewData }: LeagueWalletTemplateProps) {
{/* Balance Card */} {/* Balance Card */}
<Card> <Card>
<Stack direction="row" align="center" gap={4}> <Stack direction="row" align="center" gap={4}>
<Surface variant="muted" rounded="xl" padding={3} style={{ backgroundColor: 'rgba(59, 130, 246, 0.1)' }}> <Surface variant="muted" rounded="xl" padding={3} bg="bg-blue-500/10">
<Icon icon={Wallet} size={6} color="#3b82f6" /> <Icon icon={Wallet} size={6} color="#3b82f6" />
</Surface> </Surface>
<Box> <Box>
@@ -47,7 +47,7 @@ export function LeagueWalletTemplate({ viewData }: LeagueWalletTemplateProps) {
<Card> <Card>
<Stack gap={4}> <Stack gap={4}>
<Stack direction="row" align="center" gap={3}> <Stack direction="row" align="center" gap={3}>
<Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: 'rgba(16, 185, 129, 0.1)' }}> <Surface variant="muted" rounded="lg" padding={2} bg="bg-amber-500/10">
<Icon icon={Calendar} size={5} color="#10b981" /> <Icon icon={Calendar} size={5} color="#10b981" />
</Surface> </Surface>
<Box> <Box>
@@ -75,7 +75,7 @@ export function LeagueWalletTemplate({ viewData }: LeagueWalletTemplateProps) {
<Card> <Card>
<Stack gap={4}> <Stack gap={4}>
<Stack direction="row" align="center" gap={3}> <Stack direction="row" align="center" gap={3}>
<Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: 'rgba(16, 185, 129, 0.1)' }}> <Surface variant="muted" rounded="lg" padding={2} bg="bg-green-500/10">
<Icon icon={DollarSign} size={5} color="#10b981" /> <Icon icon={DollarSign} size={5} color="#10b981" />
</Surface> </Surface>
<Box> <Box>
@@ -90,10 +90,10 @@ export function LeagueWalletTemplate({ viewData }: LeagueWalletTemplateProps) {
{/* Note about features */} {/* Note about features */}
<Card> <Card>
<Stack align="center" py={8} gap={4}> <Stack align="center" py={8} gap={4}>
<Surface variant="muted" rounded="full" padding={4} style={{ backgroundColor: 'rgba(59, 130, 246, 0.1)' }}> <Surface variant="muted" rounded="full" padding={4} bg="bg-blue-500/10">
<Icon icon={Wallet} size={8} color="#3b82f6" /> <Icon icon={Wallet} size={8} color="#3b82f6" />
</Surface> </Surface>
<Box style={{ textAlign: 'center' }}> <Box textAlign="center">
<Heading level={3}>Wallet Management</Heading> <Heading level={3}>Wallet Management</Heading>
<Text size="sm" color="text-gray-400" block mt={2}> <Text size="sm" color="text-gray-400" block mt={2}>
Interactive withdrawal and export features will be implemented in future updates. Interactive withdrawal and export features will be implemented in future updates.

View File

@@ -39,7 +39,7 @@ export function ProfileLeaguesTemplate({ viewData }: ProfileLeaguesTemplateProps
{viewData.ownedLeagues.length === 0 ? ( {viewData.ownedLeagues.length === 0 ? (
<Text size="sm" color="text-gray-400"> <Text size="sm" color="text-gray-400">
You don't own any leagues yet in this session. You don&apos;t own any leagues yet in this session.
</Text> </Text>
) : ( ) : (
<Stack gap={3}> <Stack gap={3}>
@@ -55,7 +55,7 @@ export function ProfileLeaguesTemplate({ viewData }: ProfileLeaguesTemplateProps
<Surface variant="muted" rounded="lg" border padding={6}> <Surface variant="muted" rounded="lg" border padding={6}>
<Stack gap={4}> <Stack gap={4}>
<Stack direction="row" align="center" justify="between"> <Stack direction="row" align="center" justify="between">
<Heading level={2}>Leagues you're in</Heading> <Heading level={2}>Leagues you&apos;re in</Heading>
{viewData.memberLeagues.length > 0 && ( {viewData.memberLeagues.length > 0 && (
<Text size="xs" color="text-gray-400"> <Text size="xs" color="text-gray-400">
{viewData.memberLeagues.length} {viewData.memberLeagues.length === 1 ? 'league' : 'leagues'} {viewData.memberLeagues.length} {viewData.memberLeagues.length === 1 ? 'league' : 'leagues'}
@@ -65,7 +65,7 @@ export function ProfileLeaguesTemplate({ viewData }: ProfileLeaguesTemplateProps
{viewData.memberLeagues.length === 0 ? ( {viewData.memberLeagues.length === 0 ? (
<Text size="sm" color="text-gray-400"> <Text size="sm" color="text-gray-400">
You're not a member of any other leagues yet. You&apos;re not a member of any other leagues yet.
</Text> </Text>
) : ( ) : (
<Stack gap={3}> <Stack gap={3}>

View File

@@ -7,7 +7,7 @@ import { ProfileSettings } from '@/components/drivers/ProfileSettings';
import { AchievementGrid } from '@/ui/AchievementGrid'; import { AchievementGrid } from '@/ui/AchievementGrid';
import { ProfileHero } from '@/ui/ProfileHero'; import { ProfileHero } from '@/ui/ProfileHero';
import { ProfileStatGrid } from '@/ui/ProfileStatGrid'; import { ProfileStatGrid } from '@/ui/ProfileStatGrid';
import { ProfileTabs } from '@/ui/ProfileTabs'; import { ProfileTabs, type ProfileTab as ProfileTabsType } from '@/ui/ProfileTabs';
import { TeamMembershipGrid } from '@/ui/TeamMembershipGrid'; import { TeamMembershipGrid } from '@/ui/TeamMembershipGrid';
import type { ProfileViewData } from '@/lib/view-data/ProfileViewData'; import type { ProfileViewData } from '@/lib/view-data/ProfileViewData';
import { Box } from '@/ui/Box'; import { Box } from '@/ui/Box';
@@ -74,7 +74,7 @@ export function ProfileTemplate({
Create your driver profile to join leagues, compete in races, and connect with other drivers. Create your driver profile to join leagues, compete in races, and connect with other drivers.
</Text> </Text>
</Box> </Box>
<CreateDriverForm /> <CreateDriverForm onSuccess={() => {}} isPending={false} />
</Stack> </Stack>
</Card> </Card>
</Box> </Box>
@@ -150,7 +150,7 @@ export function ProfileTemplate({
stats={viewData.stats ? { rating: Number(viewData.stats.ratingLabel) || 0 } : null} stats={viewData.stats ? { rating: Number(viewData.stats.ratingLabel) || 0 } : null}
globalRank={Number(viewData.stats?.globalRankLabel) || 0} globalRank={Number(viewData.stats?.globalRankLabel) || 0}
timezone={viewData.extendedProfile?.timezone || 'UTC'} timezone={viewData.extendedProfile?.timezone || 'UTC'}
socialHandles={viewData.extendedProfile?.socialHandles.map(s => ({ ...s, platform: s.platformLabel as any })) || []} socialHandles={viewData.extendedProfile?.socialHandles.map(s => ({ ...s, platform: s.platformLabel })) || []}
onAddFriend={onFriendRequestSend} onAddFriend={onFriendRequestSend}
friendRequestSent={friendRequestSent} friendRequestSent={friendRequestSent}
/> />
@@ -176,7 +176,7 @@ export function ProfileTemplate({
/> />
)} )}
<ProfileTabs activeTab={activeTab as any} onTabChange={onTabChange as any} /> <ProfileTabs activeTab={activeTab as unknown as ProfileTabsType} onTabChange={onTabChange as unknown as (tab: ProfileTabsType) => void} />
{activeTab === 'history' && ( {activeTab === 'history' && (
<Card> <Card>

View File

@@ -12,20 +12,19 @@ import { Container } from '@/ui/Container';
import { Grid } from '@/ui/Grid'; import { Grid } from '@/ui/Grid';
import { Icon } from '@/ui/Icon'; import { Icon } from '@/ui/Icon';
import { Surface } from '@/ui/Surface'; import { Surface } from '@/ui/Surface';
import { ArrowLeft, Trophy, Zap } from 'lucide-react'; import { ArrowLeft, Trophy, Zap, type LucideIcon } from 'lucide-react';
import type { RaceResultsViewData } from '@/lib/view-data/races/RaceResultsViewData'; import type { RaceResultsViewData } from '@/lib/view-data/races/RaceResultsViewData';
import { RaceResultRow } from '@/ui/RaceResultRow'; import { RaceResultRow } from '@/ui/RaceResultRow';
import { RacePenaltyRow } from '@/ui/RacePenaltyRowWrapper'; import { RacePenaltyRow } from '@/ui/RacePenaltyRowWrapper';
export interface RaceResultsTemplateProps { export interface RaceResultsTemplateProps {
viewData: RaceResultsViewData; viewData: RaceResultsViewData;
currentDriverId: string;
isAdmin: boolean; isAdmin: boolean;
isLoading: boolean; isLoading: boolean;
error?: Error | null; error?: Error | null;
// Actions // Actions
onBack: () => void; onBack: () => void;
onImportResults: (results: any[]) => void; onImportResults: (results: unknown[]) => void;
onPenaltyClick: (driver: { id: string; name: string }) => void; onPenaltyClick: (driver: { id: string; name: string }) => void;
// UI State // UI State
importing: boolean; importing: boolean;
@@ -37,7 +36,6 @@ export interface RaceResultsTemplateProps {
export function RaceResultsTemplate({ export function RaceResultsTemplate({
viewData, viewData,
currentDriverId,
isLoading, isLoading,
error, error,
onBack, onBack,
@@ -114,9 +112,9 @@ export function RaceResultsTemplate({
</Stack> </Stack>
{/* Header */} {/* Header */}
<Surface variant="muted" rounded="xl" border padding={6} style={{ background: 'linear-gradient(to right, rgba(38, 38, 38, 0.5), rgba(38, 38, 38, 0.3))', borderColor: '#262626' }}> <Surface variant="muted" rounded="xl" border padding={6} bg="bg-neutral-800/50" borderColor="border-neutral-800">
<Stack direction="row" align="center" gap={4} mb={6}> <Stack direction="row" align="center" gap={4} mb={6}>
<Surface variant="muted" rounded="xl" padding={3} style={{ backgroundColor: 'rgba(59, 130, 246, 0.2)' }}> <Surface variant="muted" rounded="xl" padding={3} bg="bg-blue-500/20">
<Icon icon={Trophy} size={6} color="#3b82f6" /> <Icon icon={Trophy} size={6} color="#3b82f6" />
</Surface> </Surface>
<Box> <Box>
@@ -131,20 +129,20 @@ export function RaceResultsTemplate({
<Grid cols={4} gap={4}> <Grid cols={4} gap={4}>
<StatItem label="Drivers" value={viewData.totalDrivers ?? 0} /> <StatItem label="Drivers" value={viewData.totalDrivers ?? 0} />
<StatItem label="League" value={viewData.leagueName ?? '—'} /> <StatItem label="League" value={viewData.leagueName ?? '—'} />
<StatItem label="SOF" value={viewData.raceSOF ?? '—'} icon={Zap} color="#f59e0b" /> <StatItem label="SOF" value={viewData.raceSOF ?? '—'} icon={Zap} color="text-warning-amber" />
<StatItem label="Fastest Lap" value={viewData.fastestLapTime ? formatTime(viewData.fastestLapTime) : '—'} color="#10b981" /> <StatItem label="Fastest Lap" value={viewData.fastestLapTime ? formatTime(viewData.fastestLapTime) : '—'} color="text-performance-green" />
</Grid> </Grid>
</Surface> </Surface>
{importSuccess && ( {importSuccess && (
<Surface variant="muted" rounded="lg" border padding={4} style={{ backgroundColor: 'rgba(16, 185, 129, 0.1)', borderColor: 'rgba(16, 185, 129, 0.3)' }}> <Surface variant="muted" rounded="lg" border padding={4} bg="bg-green-500/10" borderColor="border-green-500/30">
<Text color="text-performance-green" weight="bold">Success!</Text> <Text color="text-performance-green" weight="bold">Success!</Text>
<Text color="text-performance-green" size="sm" block mt={1}>Results imported and standings updated.</Text> <Text color="text-performance-green" size="sm" block mt={1}>Results imported and standings updated.</Text>
</Surface> </Surface>
)} )}
{importError && ( {importError && (
<Surface variant="muted" rounded="lg" border padding={4} style={{ backgroundColor: 'rgba(239, 68, 68, 0.1)', borderColor: 'rgba(239, 68, 68, 0.3)' }}> <Surface variant="muted" rounded="lg" border padding={4} bg="bg-red-500/10" borderColor="border-red-500/30">
<Text color="text-error-red" weight="bold">Error:</Text> <Text color="text-error-red" weight="bold">Error:</Text>
<Text color="text-error-red" size="sm" block mt={1}>{importError}</Text> <Text color="text-error-red" size="sm" block mt={1}>{importError}</Text>
</Surface> </Surface>
@@ -158,7 +156,7 @@ export function RaceResultsTemplate({
{viewData.results.map((result) => ( {viewData.results.map((result) => (
<RaceResultRow <RaceResultRow
key={result.driverId} key={result.driverId}
result={result as any} result={result as unknown as never}
points={viewData.pointsSystem[result.position.toString()] ?? 0} points={viewData.pointsSystem[result.position.toString()] ?? 0}
/> />
))} ))}
@@ -166,13 +164,13 @@ export function RaceResultsTemplate({
{/* Penalties Section */} {/* Penalties Section */}
{viewData.penalties.length > 0 && ( {viewData.penalties.length > 0 && (
<Box pt={6} style={{ borderTop: '1px solid #262626' }}> <Box pt={6} borderTop="1px solid" borderColor="border-neutral-800">
<Box mb={4}> <Box mb={4}>
<Heading level={2}>Penalties</Heading> <Heading level={2}>Penalties</Heading>
</Box> </Box>
<Stack gap={2}> <Stack gap={2}>
{viewData.penalties.map((penalty, index) => ( {viewData.penalties.map((penalty, index) => (
<RacePenaltyRow key={index} penalty={penalty as any} /> <RacePenaltyRow key={index} penalty={penalty as unknown as never} />
))} ))}
</Stack> </Stack>
</Box> </Box>
@@ -215,13 +213,13 @@ export function RaceResultsTemplate({
); );
} }
function StatItem({ label, value, icon, color = 'text-white' }: { label: string, value: string | number, icon?: any, color?: string }) { function StatItem({ label, value, icon, color = 'text-white' }: { label: string, value: string | number, icon?: LucideIcon, color?: string }) {
return ( return (
<Surface variant="muted" rounded="lg" padding={3} style={{ backgroundColor: 'rgba(15, 17, 21, 0.6)' }}> <Surface variant="muted" rounded="lg" padding={3} bg="bg-neutral-900/60">
<Text size="xs" color="text-gray-500" block mb={1}>{label}</Text> <Text size="xs" color="text-gray-500" block mb={1}>{label}</Text>
<Stack direction="row" align="center" gap={1.5}> <Stack direction="row" align="center" gap={1.5}>
{icon && <Icon icon={icon} size={4} color={color === 'text-white' ? '#9ca3af' : color} />} {icon && <Icon icon={icon} size={4} color={color === 'text-white' ? '#9ca3af' : color} />}
<Text weight="bold" color={color as any} style={{ fontSize: '1.125rem' }}>{value}</Text> <Text weight="bold" color={color} size="lg">{value}</Text>
</Stack> </Stack>
</Surface> </Surface>
); );

View File

@@ -44,7 +44,6 @@ interface RaceStewardingTemplateProps {
export function RaceStewardingTemplate({ export function RaceStewardingTemplate({
viewData, viewData,
isLoading, isLoading,
error,
onBack, onBack,
onReviewProtest, onReviewProtest,
isAdmin, isAdmin,
@@ -77,9 +76,9 @@ export function RaceStewardingTemplate({
<Surface variant="muted" rounded="full" padding={4}> <Surface variant="muted" rounded="full" padding={4}>
<Icon icon={AlertTriangle} size={8} color="#f59e0b" /> <Icon icon={AlertTriangle} size={8} color="#f59e0b" />
</Surface> </Surface>
<Box style={{ textAlign: 'center' }}> <Box textAlign="center">
<Text weight="medium" color="text-white" block mb={1}>Race not found</Text> <Text weight="medium" color="text-white" block mb={1}>Race not found</Text>
<Text size="sm" color="text-gray-500">The race you're looking for doesn't exist.</Text> <Text size="sm" color="text-gray-500">The race you&apos;re looking for doesn&apos;t exist.</Text>
</Box> </Box>
<Button variant="secondary" onClick={onBack}> <Button variant="secondary" onClick={onBack}>
Back to Races Back to Races
@@ -112,9 +111,9 @@ export function RaceStewardingTemplate({
</Stack> </Stack>
{/* Header */} {/* Header */}
<Surface variant="muted" rounded="xl" border padding={6} style={{ background: 'linear-gradient(to right, rgba(38, 38, 38, 0.5), rgba(38, 38, 38, 0.3))', borderColor: '#262626' }}> <Surface variant="muted" rounded="xl" border padding={6} bg="bg-gradient-to-r from-neutral-800/50 to-neutral-800/30" borderColor="border-neutral-800">
<Stack direction="row" align="center" gap={4} mb={6}> <Stack direction="row" align="center" gap={4} mb={6}>
<Surface variant="muted" rounded="xl" padding={3} style={{ backgroundColor: 'rgba(59, 130, 246, 0.2)' }}> <Surface variant="muted" rounded="xl" padding={3} bg="bg-blue-500/20">
<Icon icon={Scale} size={6} color="#3b82f6" /> <Icon icon={Scale} size={6} color="#3b82f6" />
</Surface> </Surface>
<Box> <Box>
@@ -149,7 +148,7 @@ export function RaceStewardingTemplate({
<Surface variant="muted" rounded="full" padding={4}> <Surface variant="muted" rounded="full" padding={4}>
<Icon icon={Flag} size={8} color="#10b981" /> <Icon icon={Flag} size={8} color="#10b981" />
</Surface> </Surface>
<Box style={{ textAlign: 'center' }}> <Box textAlign="center">
<Text weight="semibold" size="lg" color="text-white" block mb={1}>All Clear!</Text> <Text weight="semibold" size="lg" color="text-white" block mb={1}>All Clear!</Text>
<Text size="sm" color="text-gray-400">No pending protests to review</Text> <Text size="sm" color="text-gray-400">No pending protests to review</Text>
</Box> </Box>
@@ -159,7 +158,7 @@ export function RaceStewardingTemplate({
viewData.pendingProtests.map((protest) => ( viewData.pendingProtests.map((protest) => (
<ProtestCard <ProtestCard
key={protest.id} key={protest.id}
protest={protest as any} protest={protest}
protester={viewData.driverMap[protest.protestingDriverId]} protester={viewData.driverMap[protest.protestingDriverId]}
accused={viewData.driverMap[protest.accusedDriverId]} accused={viewData.driverMap[protest.accusedDriverId]}
isAdmin={isAdmin} isAdmin={isAdmin}
@@ -179,7 +178,7 @@ export function RaceStewardingTemplate({
<Surface variant="muted" rounded="full" padding={4}> <Surface variant="muted" rounded="full" padding={4}>
<Icon icon={CheckCircle} size={8} color="#525252" /> <Icon icon={CheckCircle} size={8} color="#525252" />
</Surface> </Surface>
<Box style={{ textAlign: 'center' }}> <Box textAlign="center">
<Text weight="semibold" size="lg" color="text-white" block mb={1}>No Resolved Protests</Text> <Text weight="semibold" size="lg" color="text-white" block mb={1}>No Resolved Protests</Text>
<Text size="sm" color="text-gray-400">Resolved protests will appear here</Text> <Text size="sm" color="text-gray-400">Resolved protests will appear here</Text>
</Box> </Box>
@@ -189,7 +188,7 @@ export function RaceStewardingTemplate({
viewData.resolvedProtests.map((protest) => ( viewData.resolvedProtests.map((protest) => (
<ProtestCard <ProtestCard
key={protest.id} key={protest.id}
protest={protest as any} protest={protest}
protester={viewData.driverMap[protest.protestingDriverId]} protester={viewData.driverMap[protest.protestingDriverId]}
accused={viewData.driverMap[protest.accusedDriverId]} accused={viewData.driverMap[protest.accusedDriverId]}
isAdmin={isAdmin} isAdmin={isAdmin}
@@ -209,7 +208,7 @@ export function RaceStewardingTemplate({
<Surface variant="muted" rounded="full" padding={4}> <Surface variant="muted" rounded="full" padding={4}>
<Icon icon={Gavel} size={8} color="#525252" /> <Icon icon={Gavel} size={8} color="#525252" />
</Surface> </Surface>
<Box style={{ textAlign: 'center' }}> <Box textAlign="center">
<Text weight="semibold" size="lg" color="text-white" block mb={1}>No Penalties</Text> <Text weight="semibold" size="lg" color="text-white" block mb={1}>No Penalties</Text>
<Text size="sm" color="text-gray-400">Penalties issued for this race will appear here</Text> <Text size="sm" color="text-gray-400">Penalties issued for this race will appear here</Text>
</Box> </Box>
@@ -217,7 +216,14 @@ export function RaceStewardingTemplate({
</Card> </Card>
) : ( ) : (
viewData.penalties.map((penalty) => ( viewData.penalties.map((penalty) => (
<RacePenaltyRow key={penalty.id} penalty={penalty as any} /> <RacePenaltyRow
key={penalty.id}
penalty={{
...penalty,
driverName: viewData.driverMap[penalty.driverId]?.name || 'Unknown',
type: penalty.type as 'time_penalty' | 'grid_penalty' | 'points_deduction' | 'disqualification' | 'warning' | 'license_points'
}}
/>
)) ))
)} )}
</Stack> </Stack>

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { useMemo, useEffect } from 'react'; import React from 'react';
import { Card } from '@/ui/Card'; import { Card } from '@/ui/Card';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading'; import { Heading } from '@/ui/Heading';
@@ -26,6 +26,8 @@ export type StatusFilter = 'scheduled' | 'running' | 'completed' | 'cancelled' |
interface RacesAllTemplateProps { interface RacesAllTemplateProps {
viewData: RacesViewData; viewData: RacesViewData;
races: RacesViewData['races'];
totalFilteredCount: number;
isLoading: boolean; isLoading: boolean;
// Pagination // Pagination
currentPage: number; currentPage: number;
@@ -51,6 +53,8 @@ interface RacesAllTemplateProps {
export function RacesAllTemplate({ export function RacesAllTemplate({
viewData, viewData,
races,
totalFilteredCount,
isLoading, isLoading,
currentPage, currentPage,
totalPages, totalPages,
@@ -68,44 +72,6 @@ export function RacesAllTemplate({
setShowFilterModal, setShowFilterModal,
onRaceClick, onRaceClick,
}: RacesAllTemplateProps) { }: RacesAllTemplateProps) {
const { races } = viewData;
// Filter races
const filteredRaces = useMemo(() => {
return races.filter(race => {
if (statusFilter !== 'all' && race.status !== statusFilter) {
return false;
}
if (leagueFilter !== 'all' && race.leagueId !== leagueFilter) {
return false;
}
if (searchQuery) {
const query = searchQuery.toLowerCase();
const matchesTrack = race.track.toLowerCase().includes(query);
const matchesCar = race.car.toLowerCase().includes(query);
const matchesLeague = race.leagueName?.toLowerCase().includes(query);
if (!matchesTrack && !matchesCar && !matchesLeague) {
return false;
}
}
return true;
});
}, [races, statusFilter, leagueFilter, searchQuery]);
// Paginate
const paginatedRaces = useMemo(() => {
const start = (currentPage - 1) * itemsPerPage;
return filteredRaces.slice(start, start + itemsPerPage);
}, [filteredRaces, currentPage, itemsPerPage]);
// Reset page when filters change
useEffect(() => {
onPageChange(1);
}, [statusFilter, leagueFilter, searchQuery]);
const breadcrumbItems = [ const breadcrumbItems = [
{ label: 'Races', href: '/races' }, { label: 'Races', href: '/races' },
{ label: 'All Races' }, { label: 'All Races' },
@@ -140,7 +106,7 @@ export function RacesAllTemplate({
All Races All Races
</Heading> </Heading>
<Text size="sm" color="text-gray-400" block mt={1}> <Text size="sm" color="text-gray-400" block mt={1}>
{filteredRaces.length} race{filteredRaces.length !== 1 ? 's' : ''} found {totalFilteredCount} race{totalFilteredCount !== 1 ? 's' : ''} found
</Text> </Text>
</Box> </Box>
@@ -170,16 +136,16 @@ export function RacesAllTemplate({
)} )}
{/* Race List */} {/* Race List */}
{paginatedRaces.length === 0 ? ( {races.length === 0 ? (
<Card> <Card>
<Stack align="center" py={12} gap={4}> <Stack align="center" py={12} gap={4}>
<Surface variant="muted" rounded="full" padding={4}> <Surface variant="muted" rounded="full" padding={4}>
<Icon icon={Calendar} size={8} color="#525252" /> <Icon icon={Calendar} size={8} color="#525252" />
</Surface> </Surface>
<Box style={{ textAlign: 'center' }}> <Box textAlign="center">
<Text weight="medium" color="text-white" block mb={1}>No races found</Text> <Text weight="medium" color="text-white" block mb={1}>No races found</Text>
<Text size="sm" color="text-gray-500"> <Text size="sm" color="text-gray-500">
{races.length === 0 {viewData.races.length === 0
? 'No races have been scheduled yet' ? 'No races have been scheduled yet'
: 'Try adjusting your search or filters'} : 'Try adjusting your search or filters'}
</Text> </Text>
@@ -188,8 +154,8 @@ export function RacesAllTemplate({
</Card> </Card>
) : ( ) : (
<Stack gap={3}> <Stack gap={3}>
{paginatedRaces.map(race => ( {races.map(race => (
<RaceListItem key={race.id} race={race as any} onClick={onRaceClick} /> <RaceListItem key={race.id} race={race} onClick={onRaceClick} />
))} ))}
</Stack> </Stack>
)} )}
@@ -198,7 +164,7 @@ export function RacesAllTemplate({
<Pagination <Pagination
currentPage={currentPage} currentPage={currentPage}
totalPages={totalPages} totalPages={totalPages}
totalItems={filteredRaces.length} totalItems={totalFilteredCount}
itemsPerPage={itemsPerPage} itemsPerPage={itemsPerPage}
onPageChange={onPageChange} onPageChange={onPageChange}
/> />

View File

@@ -100,7 +100,7 @@ export function RosterAdminTemplate({
)} )}
</Box> </Box>
<Box pt={6} style={{ borderTop: '1px solid #262626' }}> <Box pt={6} borderTop borderColor="border-neutral-800">
<Box mb={4}> <Box mb={4}>
<Heading level={2}>Members</Heading> <Heading level={2}>Members</Heading>
</Box> </Box>

View File

@@ -34,7 +34,7 @@ export function RulebookTemplate({ viewData }: RulebookTemplateProps) {
<Grid cols={4} gap={4}> <Grid cols={4} gap={4}>
<StatItem label="Platform" value={viewData.gameName} /> <StatItem label="Platform" value={viewData.gameName} />
<StatItem label="Championships" value={viewData.championshipsCount} /> <StatItem label="Championships" value={viewData.championshipsCount} />
<StatItem label="Sessions Scored" value={viewData.sessionTypes} capitalize /> <StatItem label="Sessions Scored" value={viewData.sessionTypes} />
<StatItem label="Drop Policy" value={viewData.hasActiveDropPolicy ? 'Active' : 'None'} /> <StatItem label="Drop Policy" value={viewData.hasActiveDropPolicy ? 'Active' : 'None'} />
</Grid> </Grid>
@@ -81,7 +81,7 @@ export function RulebookTemplate({ viewData }: RulebookTemplateProps) {
padding={3} padding={3}
> >
<Stack direction="row" align="center" gap={4}> <Stack direction="row" align="center" gap={4}>
<Surface variant="muted" rounded="full" padding={1} style={{ width: '2rem', height: '2rem', backgroundColor: 'rgba(16, 185, 129, 0.1)', border: '1px solid rgba(16, 185, 129, 0.2)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <Surface variant="muted" rounded="full" padding={1} w="8" h="8" bg="bg-performance-green/10" borderColor="border-performance-green/20" display="flex" alignItems="center" justifyContent="center" border>
<Text color="text-performance-green" weight="bold">+</Text> <Text color="text-performance-green" weight="bold">+</Text>
</Surface> </Surface>
<Text size="sm" color="text-gray-300">{bonus}</Text> <Text size="sm" color="text-gray-300">{bonus}</Text>
@@ -110,11 +110,11 @@ export function RulebookTemplate({ viewData }: RulebookTemplateProps) {
); );
} }
function StatItem({ label, value, capitalize }: { label: string, value: string | number, capitalize?: boolean }) { function StatItem({ label, value }: { label: string, value: string | number }) {
return ( return (
<Surface variant="muted" rounded="lg" border padding={4} style={{ backgroundColor: '#262626', borderColor: '#262626' }}> <Surface variant="muted" rounded="lg" border padding={4} bg="bg-neutral-800" borderColor="border-neutral-800">
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }} block mb={1}>{label}</Text> <Text size="xs" color="text-gray-500" uppercase letterSpacing="0.05em" block mb={1}>{label}</Text>
<Text weight="semibold" color="text-white" style={{ fontSize: '1.125rem', textTransform: capitalize ? 'capitalize' : 'none' }}>{value}</Text> <Text weight="semibold" color="text-white" size="lg">{value}</Text>
</Surface> </Surface>
); );
} }

View File

@@ -183,7 +183,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
<Stack gap={6}> <Stack gap={6}>
{/* Top Performing Sponsorships */} {/* Top Performing Sponsorships */}
<Card p={0}> <Card p={0}>
<Box p={4} style={{ borderBottom: '1px solid #262626' }}> <Box p={4} borderBottom borderColor="border-neutral-800">
<Stack direction="row" align="center" justify="between"> <Stack direction="row" align="center" justify="between">
<Heading level={3}>Top Performing</Heading> <Heading level={3}>Top Performing</Heading>
<Box> <Box>
@@ -209,7 +209,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
</Box> </Box>
</Stack> </Stack>
<Stack direction="row" align="center" gap={4}> <Stack direction="row" align="center" gap={4}>
<Box style={{ textAlign: 'right' }}> <Box textAlign="right">
<Text weight="semibold" color="text-white" block>1.2k</Text> <Text weight="semibold" color="text-white" block>1.2k</Text>
<Text size="xs" color="text-gray-500">impressions</Text> <Text size="xs" color="text-gray-500">impressions</Text>
</Box> </Box>
@@ -224,7 +224,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
{/* Upcoming Events */} {/* Upcoming Events */}
<Card p={0}> <Card p={0}>
<Box p={4} style={{ borderBottom: '1px solid #262626' }}> <Box p={4} borderBottom borderColor="border-neutral-800">
<Heading level={3} icon={<Icon icon={Calendar} size={5} color="#f59e0b" />}> <Heading level={3} icon={<Icon icon={Calendar} size={5} color="#f59e0b" />}>
Upcoming Sponsored Events Upcoming Sponsored Events
</Heading> </Heading>
@@ -338,7 +338,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
<Text color="text-gray-400">Next Invoice</Text> <Text color="text-gray-400">Next Invoice</Text>
<Text weight="medium" color="text-white">Jan 1, 2026</Text> <Text weight="medium" color="text-white">Jan 1, 2026</Text>
</Stack> </Stack>
<Box pt={3} style={{ borderTop: '1px solid #262626' }}> <Box pt={3} borderTop borderColor="border-neutral-800">
<Box> <Box>
<Link href={routes.sponsor.billing} variant="ghost"> <Link href={routes.sponsor.billing} variant="ghost">
<Button variant="secondary" fullWidth size="sm" icon={<Icon icon={CreditCard} size={4} />}> <Button variant="secondary" fullWidth size="sm" icon={<Icon icon={CreditCard} size={4} />}>

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React from 'react';
import { import {
Trophy, Trophy,
Users, Users,
@@ -13,7 +13,8 @@ import {
BarChart3, BarChart3,
Megaphone, Megaphone,
CreditCard, CreditCard,
FileText FileText,
type LucideIcon
} from 'lucide-react'; } from 'lucide-react';
import { Card } from '@/ui/Card'; import { Card } from '@/ui/Card';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
@@ -32,7 +33,7 @@ import { SponsorTierCard } from '@/components/sponsors/SponsorTierCard';
import { siteConfig } from '@/lib/siteConfig'; import { siteConfig } from '@/lib/siteConfig';
import { routes } from '@/lib/routing/RouteConfig'; import { routes } from '@/lib/routing/RouteConfig';
interface SponsorLeagueDetailData { interface SponsorLeagueDetailViewData {
league: { league: {
id: string; id: string;
name: string; name: string;
@@ -97,16 +98,23 @@ interface SponsorLeagueDetailData {
}>; }>;
} }
export type SponsorLeagueDetailTab = 'overview' | 'drivers' | 'races' | 'sponsor';
interface SponsorLeagueDetailTemplateProps { interface SponsorLeagueDetailTemplateProps {
viewData: SponsorLeagueDetailData; viewData: SponsorLeagueDetailViewData;
activeTab: SponsorLeagueDetailTab;
setActiveTab: (tab: SponsorLeagueDetailTab) => void;
selectedTier: 'main' | 'secondary';
setSelectedTier: (tier: 'main' | 'secondary') => void;
} }
type TabType = 'overview' | 'drivers' | 'races' | 'sponsor'; export function SponsorLeagueDetailTemplate({
viewData,
export function SponsorLeagueDetailTemplate({ viewData }: SponsorLeagueDetailTemplateProps) { activeTab,
const [activeTab, setActiveTab] = useState<TabType>('overview'); setActiveTab,
const [selectedTier, setSelectedTier] = useState<'main' | 'secondary'>('main'); selectedTier,
setSelectedTier
}: SponsorLeagueDetailTemplateProps) {
const league = viewData.league; const league = viewData.league;
return ( return (
@@ -129,11 +137,11 @@ export function SponsorLeagueDetailTemplate({ viewData }: SponsorLeagueDetailTem
{/* Header */} {/* Header */}
<Stack direction="row" align="start" justify="between" wrap gap={6}> <Stack direction="row" align="start" justify="between" wrap gap={6}>
<Box style={{ flex: 1 }}> <Box flexGrow={1}>
<Stack direction="row" align="center" gap={3} mb={2}> <Stack direction="row" align="center" gap={3} mb={2}>
<Badge variant="primary"> {league.tier}</Badge> <Badge variant="primary"> {league.tier}</Badge>
<Badge variant="success">Active Season</Badge> <Badge variant="success">Active Season</Badge>
<Surface variant="muted" rounded="lg" padding={1} style={{ backgroundColor: 'rgba(38, 38, 38, 0.5)', paddingLeft: '0.5rem', paddingRight: '0.5rem' }}> <Surface variant="muted" rounded="lg" padding={1} bg="bg-neutral-800/50" px={2}>
<Stack direction="row" align="center" gap={1}> <Stack direction="row" align="center" gap={1}>
<Icon icon={Star} size={3.5} color="#facc15" /> <Icon icon={Star} size={3.5} color="#facc15" />
<Text size="sm" weight="medium" color="text-white">{league.rating}</Text> <Text size="sm" weight="medium" color="text-white">{league.rating}</Text>
@@ -144,13 +152,13 @@ export function SponsorLeagueDetailTemplate({ viewData }: SponsorLeagueDetailTem
<Text color="text-gray-400" block mt={2}> <Text color="text-gray-400" block mt={2}>
{league.game} {league.season} {league.completedRaces}/{league.races} races completed {league.game} {league.season} {league.completedRaces}/{league.races} races completed
</Text> </Text>
<Text color="text-gray-400" block mt={4} style={{ maxWidth: '42rem' }}> <Text color="text-gray-400" block mt={4} maxWidth="42rem">
{league.description} {league.description}
</Text> </Text>
</Box> </Box>
<Stack direction="row" gap={3}> <Stack direction="row" gap={3}>
<Link href={`/leagues/${league.id}`}> <Link href={routes.league.detail(league.id)}>
<Button variant="secondary" icon={<Icon icon={ExternalLink} size={4} />}> <Button variant="secondary" icon={<Icon icon={ExternalLink} size={4} />}>
View League View League
</Button> </Button>
@@ -173,20 +181,19 @@ export function SponsorLeagueDetailTemplate({ viewData }: SponsorLeagueDetailTem
</Grid> </Grid>
{/* Tabs */} {/* Tabs */}
<Box style={{ borderBottom: '1px solid #262626' }}> <Box borderBottom borderColor="border-neutral-800">
<Stack direction="row" gap={6}> <Stack direction="row" gap={6}>
{(['overview', 'drivers', 'races', 'sponsor'] as const).map((tab) => ( {(['overview', 'drivers', 'races', 'sponsor'] as const).map((tab) => (
<Box <Box
key={tab} key={tab}
onClick={() => setActiveTab(tab)} onClick={() => setActiveTab(tab)}
pb={3} pb={3}
style={{ cursor="pointer"
cursor: 'pointer', borderBottom={activeTab === tab}
borderBottom: activeTab === tab ? '2px solid #3b82f6' : '2px solid transparent', borderColor={activeTab === tab ? 'border-primary-blue' : 'border-transparent'}
color: activeTab === tab ? '#3b82f6' : '#9ca3af' color={activeTab === tab ? 'text-primary-blue' : 'text-gray-400'}
}}
> >
<Text size="sm" weight="medium" style={{ textTransform: 'capitalize' }}> <Text size="sm" weight="medium" uppercase>
{tab === 'sponsor' ? '🎯 Become a Sponsor' : tab} {tab === 'sponsor' ? '🎯 Become a Sponsor' : tab}
</Text> </Text>
</Box> </Box>
@@ -235,10 +242,10 @@ export function SponsorLeagueDetailTemplate({ viewData }: SponsorLeagueDetailTem
Next Race Next Race
</Heading> </Heading>
</Box> </Box>
<Surface variant="muted" rounded="lg" border padding={4} style={{ backgroundColor: 'rgba(245, 158, 11, 0.05)', borderColor: 'rgba(245, 158, 11, 0.2)' }}> <Surface variant="muted" rounded="lg" border padding={4} bg="bg-warning-amber/5" borderColor="border-warning-amber/20">
<Stack direction="row" align="center" justify="between"> <Stack direction="row" align="center" justify="between">
<Stack direction="row" align="center" gap={4}> <Stack direction="row" align="center" gap={4}>
<Surface variant="muted" rounded="lg" padding={3} style={{ backgroundColor: 'rgba(245, 158, 11, 0.1)' }}> <Surface variant="muted" rounded="lg" padding={3} bg="bg-warning-amber/10">
<Icon icon={Flag} size={6} color="#f59e0b" /> <Icon icon={Flag} size={6} color="#f59e0b" />
</Surface> </Surface>
<Box> <Box>
@@ -259,16 +266,16 @@ export function SponsorLeagueDetailTemplate({ viewData }: SponsorLeagueDetailTem
{activeTab === 'drivers' && ( {activeTab === 'drivers' && (
<Card p={0}> <Card p={0}>
<Box p={4} style={{ borderBottom: '1px solid #262626' }}> <Box p={4} borderBottom borderColor="border-neutral-800">
<Heading level={2}>Championship Standings</Heading> <Heading level={2}>Championship Standings</Heading>
<Text size="sm" color="text-gray-400" block mt={1}>Top drivers carrying sponsor branding</Text> <Text size="sm" color="text-gray-400" block mt={1}>Top drivers carrying sponsor branding</Text>
</Box> </Box>
<Stack gap={0}> <Stack gap={0}>
{viewData.drivers.map((driver, index) => ( {viewData.drivers.map((driver, index) => (
<Box key={driver.id} p={4} style={{ borderBottom: index < viewData.drivers.length - 1 ? '1px solid rgba(38, 38, 38, 0.5)' : 'none' }}> <Box key={driver.id} p={4} borderBottom={index < viewData.drivers.length - 1} borderColor="border-neutral-800/50">
<Stack direction="row" align="center" justify="between"> <Stack direction="row" align="center" justify="between">
<Stack direction="row" align="center" gap={4}> <Stack direction="row" align="center" gap={4}>
<Surface variant="muted" rounded="full" padding={1} style={{ width: '2.5rem', height: '2.5rem', display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: '#262626' }}> <Surface variant="muted" rounded="full" padding={1} w="10" h="10" display="flex" alignItems="center" justifyContent="center" bg="bg-neutral-800">
<Text weight="bold" color="text-white">{driver.position}</Text> <Text weight="bold" color="text-white">{driver.position}</Text>
</Surface> </Surface>
<Box> <Box>
@@ -277,11 +284,11 @@ export function SponsorLeagueDetailTemplate({ viewData }: SponsorLeagueDetailTem
</Box> </Box>
</Stack> </Stack>
<Stack direction="row" align="center" gap={8}> <Stack direction="row" align="center" gap={8}>
<Box style={{ textAlign: 'right' }}> <Box textAlign="right">
<Text weight="medium" color="text-white" block>{driver.races}</Text> <Text weight="medium" color="text-white" block>{driver.races}</Text>
<Text size="xs" color="text-gray-500">races</Text> <Text size="xs" color="text-gray-500">races</Text>
</Box> </Box>
<Box style={{ textAlign: 'right' }}> <Box textAlign="right">
<Text weight="semibold" color="text-white" block>{driver.formattedImpressions}</Text> <Text weight="semibold" color="text-white" block>{driver.formattedImpressions}</Text>
<Text size="xs" color="text-gray-500">views</Text> <Text size="xs" color="text-gray-500">views</Text>
</Box> </Box>
@@ -295,16 +302,16 @@ export function SponsorLeagueDetailTemplate({ viewData }: SponsorLeagueDetailTem
{activeTab === 'races' && ( {activeTab === 'races' && (
<Card p={0}> <Card p={0}>
<Box p={4} style={{ borderBottom: '1px solid #262626' }}> <Box p={4} borderBottom borderColor="border-neutral-800">
<Heading level={2}>Race Calendar</Heading> <Heading level={2}>Race Calendar</Heading>
<Text size="sm" color="text-gray-400" block mt={1}>Season schedule with view statistics</Text> <Text size="sm" color="text-gray-400" block mt={1}>Season schedule with view statistics</Text>
</Box> </Box>
<Stack gap={0}> <Stack gap={0}>
{viewData.races.map((race, index) => ( {viewData.races.map((race, index) => (
<Box key={race.id} p={4} style={{ borderBottom: index < viewData.races.length - 1 ? '1px solid rgba(38, 38, 38, 0.5)' : 'none' }}> <Box key={race.id} p={4} borderBottom={index < viewData.races.length - 1} borderColor="border-neutral-800/50">
<Stack direction="row" align="center" justify="between"> <Stack direction="row" align="center" justify="between">
<Stack direction="row" align="center" gap={4}> <Stack direction="row" align="center" gap={4}>
<Box style={{ width: '0.75rem', height: '0.75rem', borderRadius: '9999px', backgroundColor: race.status === 'completed' ? '#10b981' : '#f59e0b' }} /> <Box w="3" h="3" rounded="full" bg={race.status === 'completed' ? 'bg-performance-green' : 'bg-warning-amber'} />
<Box> <Box>
<Text weight="medium" color="text-white" block>{race.name}</Text> <Text weight="medium" color="text-white" block>{race.name}</Text>
<Text size="sm" color="text-gray-500" block mt={1}>{race.formattedDate}</Text> <Text size="sm" color="text-gray-500" block mt={1}>{race.formattedDate}</Text>
@@ -312,7 +319,7 @@ export function SponsorLeagueDetailTemplate({ viewData }: SponsorLeagueDetailTem
</Stack> </Stack>
<Box> <Box>
{race.status === 'completed' ? ( {race.status === 'completed' ? (
<Box style={{ textAlign: 'right' }}> <Box textAlign="right">
<Text weight="semibold" color="text-white" block>{race.views.toLocaleString()}</Text> <Text weight="semibold" color="text-white" block>{race.views.toLocaleString()}</Text>
<Text size="xs" color="text-gray-500">views</Text> <Text size="xs" color="text-gray-500">views</Text>
</Box> </Box>
@@ -361,7 +368,7 @@ export function SponsorLeagueDetailTemplate({ viewData }: SponsorLeagueDetailTem
<InfoRow label="Selected Tier" value={`${selectedTier.charAt(0).toUpperCase() + selectedTier.slice(1)} Sponsor`} /> <InfoRow label="Selected Tier" value={`${selectedTier.charAt(0).toUpperCase() + selectedTier.slice(1)} Sponsor`} />
<InfoRow label="Season Price" value={`$${selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price}`} /> <InfoRow label="Season Price" value={`$${selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price}`} />
<InfoRow label={`Platform Fee (${siteConfig.fees.platformFeePercent}%)`} value={`$${((selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price) * siteConfig.fees.platformFeePercent / 100).toFixed(2)}`} /> <InfoRow label={`Platform Fee (${siteConfig.fees.platformFeePercent}%)`} value={`$${((selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price) * siteConfig.fees.platformFeePercent / 100).toFixed(2)}`} />
<Box pt={4} style={{ borderTop: '1px solid #262626' }}> <Box pt={4} borderTop borderColor="border-neutral-800">
<Stack direction="row" align="center" justify="between"> <Stack direction="row" align="center" justify="between">
<Text weight="semibold" color="text-white">Total (excl. VAT)</Text> <Text weight="semibold" color="text-white">Total (excl. VAT)</Text>
<Text size="xl" weight="bold" color="text-white"> <Text size="xl" weight="bold" color="text-white">
@@ -391,11 +398,11 @@ export function SponsorLeagueDetailTemplate({ viewData }: SponsorLeagueDetailTem
); );
} }
function StatCard({ icon, label, value, color }: { icon: any, label: string, value: string | number, color: string }) { function StatCard({ icon, label, value, color }: { icon: LucideIcon, label: string, value: string | number, color: string }) {
return ( return (
<Card> <Card>
<Stack direction="row" align="center" gap={3}> <Stack direction="row" align="center" gap={3}>
<Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: `${color}1A` }}> <Surface variant="muted" rounded="lg" padding={2} bg={`${color}1A`}>
<Icon icon={icon} size={5} color={color} /> <Icon icon={icon} size={5} color={color} />
</Surface> </Surface>
<Box> <Box>
@@ -409,10 +416,10 @@ function StatCard({ icon, label, value, color }: { icon: any, label: string, val
function InfoRow({ label, value, color = 'text-white', last }: { label: string, value: string | number, color?: string, last?: boolean }) { function InfoRow({ label, value, color = 'text-white', last }: { label: string, value: string | number, color?: string, last?: boolean }) {
return ( return (
<Box py={2} style={{ borderBottom: last ? 'none' : '1px solid rgba(38, 38, 38, 0.5)' }}> <Box py={2} borderBottom={!last} borderColor="border-neutral-800/50">
<Stack direction="row" align="center" justify="between"> <Stack direction="row" align="center" justify="between">
<Text color="text-gray-400">{label}</Text> <Text color="text-gray-400">{label}</Text>
<Text weight="medium" color={color as any}>{value}</Text> <Text weight="medium" color={color}>{value}</Text>
</Stack> </Stack>
</Box> </Box>
); );

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { useState, useMemo } from 'react'; import React from 'react';
import { Card } from '@/ui/Card'; import { Card } from '@/ui/Card';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading'; import { Heading } from '@/ui/Heading';
@@ -17,13 +17,13 @@ import {
Trophy, Trophy,
Users, Users,
Search, Search,
ChevronRight,
Car, Car,
Megaphone, Megaphone,
} from 'lucide-react'; } from 'lucide-react';
import { siteConfig } from '@/lib/siteConfig'; import { siteConfig } from '@/lib/siteConfig';
import { routes } from '@/lib/routing/RouteConfig'; import { routes } from '@/lib/routing/RouteConfig';
import { AvailableLeagueCard } from '@/components/sponsors/AvailableLeagueCard'; import { AvailableLeagueCard } from '@/components/sponsors/AvailableLeagueCard';
import { Input } from '@/ui/Input';
interface AvailableLeague { interface AvailableLeague {
id: string; id: string;
@@ -43,58 +43,34 @@ interface AvailableLeague {
cpm: number; cpm: number;
} }
type SortOption = 'rating' | 'drivers' | 'price' | 'views'; export type SortOption = 'rating' | 'drivers' | 'price' | 'views';
type TierFilter = 'all' | 'premium' | 'standard' | 'starter'; export type TierFilter = 'all' | 'premium' | 'standard' | 'starter';
type AvailabilityFilter = 'all' | 'main' | 'secondary'; export type AvailabilityFilter = 'all' | 'main' | 'secondary';
interface SponsorLeaguesTemplateProps { interface SponsorLeaguesViewData {
viewData: { leagues: AvailableLeague[];
leagues: AvailableLeague[]; stats: {
stats: { total: number;
total: number; mainAvailable: number;
mainAvailable: number; secondaryAvailable: number;
secondaryAvailable: number; totalDrivers: number;
totalDrivers: number; avgCpm: number;
avgCpm: number;
};
}; };
} }
export function SponsorLeaguesTemplate({ viewData }: SponsorLeaguesTemplateProps) { interface SponsorLeaguesTemplateProps {
const [searchQuery, setSearchQuery] = useState(''); viewData: SponsorLeaguesViewData;
const [tierFilter, setTierFilter] = useState<TierFilter>('all'); filteredLeagues: AvailableLeague[];
const [availabilityFilter, setAvailabilityFilter] = useState<AvailabilityFilter>('all'); searchQuery: string;
const [sortBy, setSortBy] = useState<SortOption>('rating'); setSearchQuery: (query: string) => void;
}
// Filter and sort leagues
const filteredLeagues = useMemo(() => {
return viewData.leagues
.filter((league) => {
if (searchQuery && !league.name.toLowerCase().includes(searchQuery.toLowerCase())) {
return false;
}
if (tierFilter !== 'all' && league.tier !== tierFilter) {
return false;
}
if (availabilityFilter === 'main' && !league.mainSponsorSlot.available) {
return false;
}
if (availabilityFilter === 'secondary' && league.secondarySlots.available === 0) {
return false;
}
return true;
})
.sort((a, b) => {
switch (sortBy) {
case 'rating': return b.rating - a.rating;
case 'drivers': return b.drivers - a.drivers;
case 'price': return a.mainSponsorSlot.price - b.mainSponsorSlot.price;
case 'views': return b.avgViewsPerRace - a.avgViewsPerRace;
default: return 0;
}
});
}, [viewData.leagues, searchQuery, tierFilter, availabilityFilter, sortBy]);
export function SponsorLeaguesTemplate({
viewData,
filteredLeagues,
searchQuery,
setSearchQuery,
}: SponsorLeaguesTemplateProps) {
const stats = viewData.stats; const stats = viewData.stats;
return ( return (
@@ -138,12 +114,11 @@ export function SponsorLeaguesTemplate({ viewData }: SponsorLeaguesTemplateProps
</Text> </Text>
<Grid cols={4} gap={4}> <Grid cols={4} gap={4}>
<Box> <Box>
<input <Input
type="text"
placeholder="Search leagues..." placeholder="Search leagues..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full px-4 py-2 rounded-lg border border-charcoal-outline bg-iron-gray text-white placeholder-gray-500 focus:border-primary-blue focus:outline-none" icon={<Icon icon={Search} size={4} />}
/> />
</Box> </Box>
{/* Selects would go here, using standard Select UI if available */} {/* Selects would go here, using standard Select UI if available */}
@@ -175,7 +150,7 @@ export function SponsorLeaguesTemplate({ viewData }: SponsorLeaguesTemplateProps
<Grid cols={3} gap={6}> <Grid cols={3} gap={6}>
{filteredLeagues.map((league) => ( {filteredLeagues.map((league) => (
<GridItem key={league.id} colSpan={12} mdSpan={6} lgSpan={4}> <GridItem key={league.id} colSpan={12} mdSpan={6} lgSpan={4}>
<AvailableLeagueCard league={league as any} /> <AvailableLeagueCard league={league} />
</GridItem> </GridItem>
))} ))}
</Grid> </Grid>
@@ -185,14 +160,12 @@ export function SponsorLeaguesTemplate({ viewData }: SponsorLeaguesTemplateProps
<Surface variant="muted" rounded="full" padding={4}> <Surface variant="muted" rounded="full" padding={4}>
<Icon icon={Trophy} size={12} color="#525252" /> <Icon icon={Trophy} size={12} color="#525252" />
</Surface> </Surface>
<Box style={{ textAlign: 'center' }}> <Box textAlign="center">
<Heading level={3}>No leagues found</Heading> <Heading level={3}>No leagues found</Heading>
<Text color="text-gray-400" block mt={2}>Try adjusting your filters to see more results</Text> <Text color="text-gray-400" block mt={2}>Try adjusting your filters to see more results</Text>
</Box> </Box>
<Button variant="secondary" onClick={() => { <Button variant="secondary" onClick={() => {
setSearchQuery(''); setSearchQuery('');
setTierFilter('all');
setAvailabilityFilter('all');
}}> }}>
Clear Filters Clear Filters
</Button> </Button>
@@ -201,7 +174,7 @@ export function SponsorLeaguesTemplate({ viewData }: SponsorLeaguesTemplateProps
)} )}
{/* Platform Fee Notice */} {/* Platform Fee Notice */}
<Surface variant="muted" rounded="lg" border padding={4} style={{ backgroundColor: 'rgba(38, 38, 38, 0.3)' }}> <Surface variant="muted" rounded="lg" border padding={4} bg="bg-neutral-800/30">
<Stack direction="row" align="start" gap={3}> <Stack direction="row" align="start" gap={3}>
<Icon icon={Megaphone} size={5} color="#3b82f6" /> <Icon icon={Megaphone} size={5} color="#3b82f6" />
<Box> <Box>
@@ -220,8 +193,8 @@ export function SponsorLeaguesTemplate({ viewData }: SponsorLeaguesTemplateProps
function StatCard({ label, value, color = 'text-white' }: { label: string, value: string | number, color?: string }) { function StatCard({ label, value, color = 'text-white' }: { label: string, value: string | number, color?: string }) {
return ( return (
<Card> <Card>
<Box style={{ textAlign: 'center' }}> <Box textAlign="center">
<Text size="2xl" weight="bold" color={color as any} block mb={1}>{value}</Text> <Text size="2xl" weight="bold" color={color} block mb={1}>{value}</Text>
<Text size="sm" color="text-gray-400">{label}</Text> <Text size="sm" color="text-gray-400">{label}</Text>
</Box> </Box>
</Card> </Card>

View File

@@ -55,7 +55,7 @@ export function SponsorshipRequestsTemplate({
padding={4} padding={4}
> >
<Stack direction="row" align="center" justify="between" wrap gap={4}> <Stack direction="row" align="center" justify="between" wrap gap={4}>
<Box style={{ flex: 1, minWidth: 0 }}> <Box flexGrow={1} minWidth="0">
<Text weight="medium" color="text-white" block>{request.sponsorName}</Text> <Text weight="medium" color="text-white" block>{request.sponsorName}</Text>
{request.message && ( {request.message && (
<Text size="xs" color="text-gray-400" block mt={1}>{request.message}</Text> <Text size="xs" color="text-gray-400" block mt={1}>{request.message}</Text>

View File

@@ -30,9 +30,9 @@ export function StewardingTemplate({ viewData }: StewardingTemplateProps) {
{/* Stats summary */} {/* Stats summary */}
<Grid cols={3} gap={4}> <Grid cols={3} gap={4}>
<StatItem label="Pending" value={viewData.totalPending} color="#f59e0b" /> <StatItem label="Pending" value={viewData.totalPending} color="text-warning-amber" />
<StatItem label="Resolved" value={viewData.totalResolved} color="#10b981" /> <StatItem label="Resolved" value={viewData.totalResolved} color="text-performance-green" />
<StatItem label="Penalties" value={viewData.totalPenalties} color="#ef4444" /> <StatItem label="Penalties" value={viewData.totalPenalties} color="text-error-red" />
</Grid> </Grid>
{/* Content */} {/* Content */}
@@ -41,7 +41,7 @@ export function StewardingTemplate({ viewData }: StewardingTemplateProps) {
<Surface variant="muted" rounded="full" padding={4}> <Surface variant="muted" rounded="full" padding={4}>
<Icon icon={Flag} size={8} color="#10b981" /> <Icon icon={Flag} size={8} color="#10b981" />
</Surface> </Surface>
<Box style={{ textAlign: 'center' }}> <Box textAlign="center">
<Text weight="semibold" size="lg" color="text-white" block mb={1}>All Clear!</Text> <Text weight="semibold" size="lg" color="text-white" block mb={1}>All Clear!</Text>
<Text size="sm" color="text-gray-400">No protests or penalties to review.</Text> <Text size="sm" color="text-gray-400">No protests or penalties to review.</Text>
</Box> </Box>
@@ -54,10 +54,11 @@ export function StewardingTemplate({ viewData }: StewardingTemplateProps) {
variant="muted" variant="muted"
rounded="lg" rounded="lg"
border border
style={{ overflow: 'hidden', borderColor: '#262626' }} borderColor="border-neutral-800"
overflow="hidden"
> >
{/* Race Header */} {/* Race Header */}
<Box p={4} style={{ backgroundColor: 'rgba(38, 38, 38, 0.5)', borderBottom: '1px solid #262626' }}> <Box p={4} bg="bg-neutral-800/50" borderBottom="1px solid" borderColor="border-neutral-800">
<Stack direction="row" align="center" gap={4} wrap> <Stack direction="row" align="center" gap={4} wrap>
<Stack direction="row" align="center" gap={2}> <Stack direction="row" align="center" gap={2}>
<Icon icon={MapPin} size={4} color="#9ca3af" /> <Icon icon={MapPin} size={4} color="#9ca3af" />
@@ -67,7 +68,7 @@ export function StewardingTemplate({ viewData }: StewardingTemplateProps) {
<Icon icon={Calendar} size={4} color="#9ca3af" /> <Icon icon={Calendar} size={4} color="#9ca3af" />
<Text size="sm" color="text-gray-400">{new Date(race.scheduledAt).toLocaleDateString()}</Text> <Text size="sm" color="text-gray-400">{new Date(race.scheduledAt).toLocaleDateString()}</Text>
</Stack> </Stack>
<Surface variant="muted" rounded="full" padding={1} style={{ backgroundColor: 'rgba(245, 158, 11, 0.1)', paddingLeft: '0.5rem', paddingRight: '0.5rem' }}> <Surface variant="muted" rounded="full" padding={1} bg="bg-amber-500/10" px={2}>
<Text size="xs" weight="medium" color="text-warning-amber">{race.pendingProtests.length} pending</Text> <Text size="xs" weight="medium" color="text-warning-amber">{race.pendingProtests.length} pending</Text>
</Surface> </Surface>
</Stack> </Stack>
@@ -77,7 +78,7 @@ export function StewardingTemplate({ viewData }: StewardingTemplateProps) {
<Box p={4}> <Box p={4}>
{race.pendingProtests.length === 0 && race.resolvedProtests.length === 0 && race.penalties.length === 0 ? ( {race.pendingProtests.length === 0 && race.resolvedProtests.length === 0 && race.penalties.length === 0 ? (
<Box py={4}> <Box py={4}>
<Text size="sm" color="text-gray-400" block style={{ textAlign: 'center' }}>No items to display</Text> <Text size="sm" color="text-gray-400" block align="center">No items to display</Text>
</Box> </Box>
) : ( ) : (
<Stack gap={3}> <Stack gap={3}>
@@ -92,16 +93,17 @@ export function StewardingTemplate({ viewData }: StewardingTemplateProps) {
rounded="lg" rounded="lg"
border border
padding={4} padding={4}
style={{ backgroundColor: 'rgba(38, 38, 38, 0.3)', borderColor: '#262626' }} bg="bg-neutral-800/30"
borderColor="border-neutral-800"
> >
<Stack direction="row" align="start" justify="between" gap={4}> <Stack direction="row" align="start" justify="between" gap={4}>
<Box style={{ flex: 1, minWidth: 0 }}> <Box flex={1} minWidth="0">
<Stack direction="row" align="center" gap={2} mb={2} wrap> <Stack direction="row" align="center" gap={2} mb={2} wrap>
<Icon icon={AlertCircle} size={4} color="#f59e0b" /> <Icon icon={AlertCircle} size={4} color="#f59e0b" />
<Text weight="medium" color="text-white"> <Text weight="medium" color="text-white">
{protester?.name || 'Unknown'} vs {accused?.name || 'Unknown'} {protester?.name || 'Unknown'} vs {accused?.name || 'Unknown'}
</Text> </Text>
<Surface variant="muted" rounded="full" padding={1} style={{ backgroundColor: 'rgba(245, 158, 11, 0.1)', paddingLeft: '0.5rem', paddingRight: '0.5rem' }}> <Surface variant="muted" rounded="full" padding={1} bg="bg-amber-500/10" px={2}>
<Text size="xs" weight="medium" color="text-warning-amber">Pending</Text> <Text size="xs" weight="medium" color="text-warning-amber">Pending</Text>
</Surface> </Surface>
</Stack> </Stack>
@@ -127,27 +129,28 @@ export function StewardingTemplate({ viewData }: StewardingTemplateProps) {
rounded="lg" rounded="lg"
border border
padding={4} padding={4}
style={{ backgroundColor: 'rgba(38, 38, 38, 0.3)', borderColor: '#262626' }} bg="bg-neutral-800/30"
borderColor="border-neutral-800"
> >
<Stack direction="row" align="center" justify="between" gap={4}> <Stack direction="row" align="center" justify="between" gap={4}>
<Stack direction="row" align="center" gap={3}> <Stack direction="row" align="center" gap={3}>
<Surface variant="muted" rounded="full" padding={2} style={{ backgroundColor: 'rgba(239, 68, 68, 0.1)' }}> <Surface variant="muted" rounded="full" padding={2} bg="bg-red-500/10">
<Icon icon={Gavel} size={4} color="#ef4444" /> <Icon icon={Gavel} size={4} color="#ef4444" />
</Surface> </Surface>
<Box> <Box>
<Stack direction="row" align="center" gap={2}> <Stack direction="row" align="center" gap={2}>
<Text weight="medium" color="text-white">{driver?.name || 'Unknown'}</Text> <Text weight="medium" color="text-white">{driver?.name || 'Unknown'}</Text>
<Surface variant="muted" rounded="full" padding={1} style={{ backgroundColor: 'rgba(239, 68, 68, 0.1)', paddingLeft: '0.5rem', paddingRight: '0.5rem' }}> <Surface variant="muted" rounded="full" padding={1} bg="bg-red-500/10" px={2}>
<Text size="xs" weight="medium" color="text-error-red" style={{ textTransform: 'capitalize' }}> <Text size="xs" weight="medium" color="text-error-red">
{penalty.type.replace('_', ' ')} {penalty.type.replace('_', ' ').toUpperCase()}
</Text> </Text>
</Surface> </Surface>
</Stack> </Stack>
<Text size="sm" color="text-gray-400" block mt={1}>{penalty.reason}</Text> <Text size="sm" color="text-gray-400" block mt={1}>{penalty.reason}</Text>
</Box> </Box>
</Stack> </Stack>
<Box style={{ textAlign: 'right' }}> <Box textAlign="right">
<Text weight="bold" color="text-error-red" style={{ fontSize: '1.125rem' }}> <Text weight="bold" color="text-error-red" size="lg">
{penalty.type === 'time_penalty' && `+${penalty.value}s`} {penalty.type === 'time_penalty' && `+${penalty.value}s`}
{penalty.type === 'grid_penalty' && `+${penalty.value} grid`} {penalty.type === 'grid_penalty' && `+${penalty.value} grid`}
{penalty.type === 'points_deduction' && `-${penalty.value} pts`} {penalty.type === 'points_deduction' && `-${penalty.value} pts`}
@@ -175,9 +178,11 @@ export function StewardingTemplate({ viewData }: StewardingTemplateProps) {
function StatItem({ label, value, color }: { label: string, value: string | number, color: string }) { function StatItem({ label, value, color }: { label: string, value: string | number, color: string }) {
return ( return (
<Surface variant="muted" rounded="lg" border padding={4} style={{ backgroundColor: 'rgba(38, 38, 38, 0.5)', borderColor: '#262626', textAlign: 'center' }}> <Surface variant="muted" rounded="lg" border padding={4} bg="bg-neutral-800/50" borderColor="border-neutral-800">
<Text size="2xl" weight="bold" style={{ color }}>{value}</Text> <Box textAlign="center">
<Text size="sm" color="text-gray-500" block mt={1}>{label}</Text> <Text size="2xl" weight="bold" color={color}>{value}</Text>
<Text size="sm" color="text-gray-400" block mt={1}>{label}</Text>
</Box>
</Surface> </Surface>
); );
} }

View File

@@ -49,6 +49,8 @@ export function TeamDetailTemplate({
}: TeamDetailTemplateProps) { }: TeamDetailTemplateProps) {
const isSponsorMode = useSponsorMode(); const isSponsorMode = useSponsorMode();
const team = viewData.team;
// Show loading state // Show loading state
if (loading) { if (loading) {
return ( return (
@@ -61,7 +63,7 @@ export function TeamDetailTemplate({
} }
// Show not found state // Show not found state
if (!viewData.team) { if (!team) {
return ( return (
<Container size="md" py={12}> <Container size="md" py={12}>
<Card> <Card>
@@ -69,7 +71,7 @@ export function TeamDetailTemplate({
<Box textAlign="center"> <Box textAlign="center">
<Heading level={1}>Team Not Found</Heading> <Heading level={1}>Team Not Found</Heading>
<Text color="text-gray-400" block mt={2}> <Text color="text-gray-400" block mt={2}>
The team you're looking for doesn't exist or has been disbanded. The team you&apos;re looking for doesn&apos;t exist or has been disbanded.
</Text> </Text>
</Box> </Box>
<Button variant="primary" onClick={onGoBack}> <Button variant="primary" onClick={onGoBack}>
@@ -81,15 +83,6 @@ export function TeamDetailTemplate({
); );
} }
const tabs: { id: Tab; label: string; visible: boolean }[] = [
{ id: 'overview', label: 'Overview', visible: true },
{ id: 'roster', label: 'Roster', visible: true },
{ id: 'standings', label: 'Standings', visible: true },
{ id: 'admin', label: 'Admin', visible: viewData.isAdmin },
];
const visibleTabs = tabs.filter(tab => tab.visible);
return ( return (
<Container size="lg" py={8}> <Container size="lg" py={8}>
<Stack gap={6}> <Stack gap={6}>
@@ -98,16 +91,16 @@ export function TeamDetailTemplate({
items={[ items={[
{ label: 'Home', href: '/' }, { label: 'Home', href: '/' },
{ label: 'Teams', href: '/teams' }, { label: 'Teams', href: '/teams' },
{ label: viewData.team.name } { label: team.name }
]} ]}
/> />
{/* Sponsor Insights Card */} {/* Sponsor Insights Card */}
{isSponsorMode && viewData.team && ( {isSponsorMode && team && (
<SponsorInsightsCard <SponsorInsightsCard
entityType="team" entityType="team"
entityId={viewData.team.id} entityId={team.id}
entityName={viewData.team.name} entityName={team.name}
tier="standard" tier="standard"
metrics={viewData.teamMetrics} metrics={viewData.teamMetrics}
slots={SlotTemplates.team(true, true, 500, 250)} slots={SlotTemplates.team(true, true, 500, 250)}
@@ -119,8 +112,8 @@ export function TeamDetailTemplate({
<TeamHero <TeamHero
team={{ team={{
...viewData.team, ...team,
leagues: viewData.team.leagues.map(id => ({ id })) leagues: team.leagues.map(id => ({ id }))
}} }}
memberCount={viewData.memberships.length} memberCount={viewData.memberships.length}
onUpdate={onUpdate} onUpdate={onUpdate}
@@ -129,16 +122,20 @@ export function TeamDetailTemplate({
{/* Tabs */} {/* Tabs */}
<Box borderBottom={true} borderColor="border-charcoal-outline"> <Box borderBottom={true} borderColor="border-charcoal-outline">
<Stack direction="row" gap={6}> <Stack direction="row" gap={6}>
{visibleTabs.map((tab) => ( {viewData.tabs.map((tab) => (
<Box tab.visible && (
key={tab.id} <Box
onClick={() => onTabChange(tab.id)} key={tab.id}
pb={3} onClick={() => onTabChange(tab.id)}
cursor="pointer" pb={3}
className={`transition-all ${activeTab === tab.id ? 'border-b-2 border-primary-blue text-primary-blue' : 'border-b-2 border-transparent text-gray-400'}`} cursor="pointer"
> borderBottom={activeTab === tab.id ? '2px solid' : '2px solid'}
<Text weight="medium">{tab.label}</Text> borderColor={activeTab === tab.id ? 'border-primary-blue' : 'border-transparent'}
</Box> color={activeTab === tab.id ? 'text-primary-blue' : 'text-gray-400'}
>
<Text weight="medium">{tab.label}</Text>
</Box>
)
))} ))}
</Stack> </Stack>
</Box> </Box>
@@ -152,7 +149,7 @@ export function TeamDetailTemplate({
<Box mb={4}> <Box mb={4}>
<Heading level={2}>About</Heading> <Heading level={2}>About</Heading>
</Box> </Box>
<Text color="text-gray-300" style={{ lineHeight: 1.625 }}>{viewData.team.description}</Text> <Text color="text-gray-300" leading="relaxed">{team.description}</Text>
</Card> </Card>
</GridItem> </GridItem>
@@ -163,16 +160,16 @@ export function TeamDetailTemplate({
</Box> </Box>
<Stack gap={3}> <Stack gap={3}>
<HorizontalStatItem label="Members" value={viewData.memberships.length.toString()} color="text-primary-blue" /> <HorizontalStatItem label="Members" value={viewData.memberships.length.toString()} color="text-primary-blue" />
{viewData.team.category && ( {team.category && (
<HorizontalStatItem label="Category" value={viewData.team.category} color="text-purple-400" /> <HorizontalStatItem label="Category" value={team.category} color="text-purple-400" />
)} )}
{viewData.team.leagues && viewData.team.leagues.length > 0 && ( {team.leagues && team.leagues.length > 0 && (
<HorizontalStatItem label="Leagues" value={viewData.team.leagues.length.toString()} color="text-green-400" /> <HorizontalStatItem label="Leagues" value={team.leagues.length.toString()} color="text-green-400" />
)} )}
{viewData.team.createdAt && ( {team.createdAt && (
<HorizontalStatItem <HorizontalStatItem
label="Founded" label="Founded"
value={new Date(viewData.team.createdAt).toLocaleDateString('en-US', { value={new Date(team.createdAt).toLocaleDateString('en-US', {
month: 'short', month: 'short',
year: 'numeric', year: 'numeric',
})} })}
@@ -197,7 +194,7 @@ export function TeamDetailTemplate({
{activeTab === 'roster' && ( {activeTab === 'roster' && (
<TeamRoster <TeamRoster
teamId={viewData.team.id} teamId={team.id}
memberships={viewData.memberships} memberships={viewData.memberships}
isAdmin={viewData.isAdmin} isAdmin={viewData.isAdmin}
onRemoveMember={onRemoveMember} onRemoveMember={onRemoveMember}
@@ -206,11 +203,11 @@ export function TeamDetailTemplate({
)} )}
{activeTab === 'standings' && ( {activeTab === 'standings' && (
<TeamStandings teamId={viewData.team.id} leagues={viewData.team.leagues} /> <TeamStandings teamId={team.id} leagues={team.leagues} />
)} )}
{activeTab === 'admin' && viewData.isAdmin && ( {activeTab === 'admin' && viewData.isAdmin && (
<TeamAdmin team={viewData.team} onUpdate={onUpdate} /> <TeamAdmin team={team} onUpdate={onUpdate} />
)} )}
</Box> </Box>
</Stack> </Stack>

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { useMemo } from 'react'; import React from 'react';
import { Award, ArrowLeft } from 'lucide-react'; import { Award, ArrowLeft } from 'lucide-react';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading'; import { Heading } from '@/ui/Heading';
@@ -11,18 +11,12 @@ import { Container } from '@/ui/Container';
import { Icon } from '@/ui/Icon'; import { Icon } from '@/ui/Icon';
import { ModalIcon } from '@/ui/ModalIcon'; import { ModalIcon } from '@/ui/ModalIcon';
import { TeamPodium } from '@/ui/TeamPodium'; import { TeamPodium } from '@/ui/TeamPodium';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { TeamFilter } from '@/ui/TeamFilter'; import { TeamFilter } from '@/ui/TeamFilter';
import { TeamRankingsTable } from '@/ui/TeamRankingsTable'; import { TeamRankingsTable } from '@/ui/TeamRankingsTable';
import type { TeamLeaderboardViewData, SkillLevel, SortBy } from '@/lib/view-data/TeamLeaderboardViewData';
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
type SortBy = 'rating' | 'wins' | 'winRate' | 'races';
interface TeamLeaderboardTemplateProps { interface TeamLeaderboardTemplateProps {
teams: TeamSummaryViewModel[]; viewData: TeamLeaderboardViewData;
searchQuery: string;
filterLevel: SkillLevel | 'all';
sortBy: SortBy;
onSearchChange: (query: string) => void; onSearchChange: (query: string) => void;
filterLevelChange: (level: SkillLevel | 'all') => void; filterLevelChange: (level: SkillLevel | 'all') => void;
onSortChange: (sort: SortBy) => void; onSortChange: (sort: SortBy) => void;
@@ -31,40 +25,14 @@ interface TeamLeaderboardTemplateProps {
} }
export function TeamLeaderboardTemplate({ export function TeamLeaderboardTemplate({
teams, viewData,
searchQuery,
filterLevel,
sortBy,
onSearchChange, onSearchChange,
filterLevelChange, filterLevelChange,
onSortChange, onSortChange,
onTeamClick, onTeamClick,
onBackToTeams, onBackToTeams,
}: TeamLeaderboardTemplateProps) { }: TeamLeaderboardTemplateProps) {
// Filter and sort teams const { searchQuery, filterLevel, sortBy, filteredAndSortedTeams } = viewData;
const filteredAndSortedTeams = useMemo(() => {
return teams
.filter((team) => {
if (searchQuery) {
const query = searchQuery.toLowerCase();
if (!team.name.toLowerCase().includes(query) && !(team.description ?? '').toLowerCase().includes(query)) {
return false;
}
}
if (filterLevel !== 'all' && team.performanceLevel !== filterLevel) {
return false;
}
return true;
})
.sort((a, b) => {
switch (sortBy) {
case 'rating': return 0; // Placeholder
case 'wins': return (b.totalWins || 0) - (a.totalWins || 0);
case 'races': return (b.totalRaces || 0) - (a.totalRaces || 0);
default: return 0;
}
});
}, [teams, searchQuery, filterLevel, sortBy]);
return ( return (
<Container size="lg" py={8}> <Container size="lg" py={8}>

View File

@@ -20,6 +20,7 @@ import { Link } from '@/ui/Link';
import { Surface } from '@/ui/Surface'; import { Surface } from '@/ui/Surface';
import { Icon } from '@/ui/Icon'; import { Icon } from '@/ui/Icon';
import { LoadingSpinner } from '@/ui/LoadingSpinner'; import { LoadingSpinner } from '@/ui/LoadingSpinner';
import { routes } from '@/lib/routing/RouteConfig';
import { ForgotPasswordViewData } from '@/lib/builders/view-data/types/ForgotPasswordViewData'; import { ForgotPasswordViewData } from '@/lib/builders/view-data/types/ForgotPasswordViewData';
interface ForgotPasswordTemplateProps { interface ForgotPasswordTemplateProps {
@@ -37,14 +38,14 @@ interface ForgotPasswordTemplateProps {
export function ForgotPasswordTemplate({ viewData, formActions, mutationState }: ForgotPasswordTemplateProps) { export function ForgotPasswordTemplate({ viewData, formActions, mutationState }: ForgotPasswordTemplateProps) {
return ( return (
<Box as="main" style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative' }}> <Box as="main" minHeight="100vh" display="flex" alignItems="center" justifyContent="center" position="relative">
{/* Background Pattern */} {/* Background Pattern */}
<Box style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom right, rgba(59, 130, 246, 0.05), transparent, rgba(147, 51, 234, 0.05))' }} /> <Box position="absolute" inset={0} bg="linear-gradient(to bottom right, rgba(59, 130, 246, 0.05), transparent, rgba(147, 51, 234, 0.05))" />
<Box style={{ position: 'relative', width: '100%', maxWidth: '28rem', padding: '0 1rem' }}> <Box position="relative" w="full" maxWidth="28rem" px={4}>
{/* Header */} {/* Header */}
<Box style={{ textAlign: 'center' }} mb={8}> <Box textAlign="center" mb={8}>
<Surface variant="muted" rounded="2xl" border padding={4} style={{ width: '4rem', height: '4rem', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 1rem' }}> <Surface variant="muted" rounded="2xl" border padding={4} w="4rem" h="4rem" display="flex" alignItems="center" justifyContent="center" mx="auto" mb={4}>
<Icon icon={Flag} size={8} color="#3b82f6" /> <Icon icon={Flag} size={8} color="#3b82f6" />
</Surface> </Surface>
<Heading level={1}>Reset Password</Heading> <Heading level={1}>Reset Password</Heading>
@@ -53,20 +54,20 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
</Text> </Text>
</Box> </Box>
<Card style={{ position: 'relative', overflow: 'hidden' }}> <Card position="relative" overflow="hidden">
{/* Background accent */} {/* Background accent */}
<Box style={{ position: 'absolute', top: 0, right: 0, width: '8rem', height: '8rem', background: 'linear-gradient(to bottom left, rgba(59, 130, 246, 0.1), transparent)', borderBottomLeftRadius: '9999px' }} /> <Box position="absolute" top={0} right={0} w="8rem" h="8rem" bg="linear-gradient(to bottom left, rgba(59, 130, 246, 0.1), transparent)" />
{!viewData.showSuccess ? ( {!viewData.showSuccess ? (
<form onSubmit={formActions.handleSubmit}> <Box as="form" onSubmit={formActions.handleSubmit}>
<Stack gap={5} style={{ position: 'relative' }}> <Stack gap={5} position="relative">
{/* Email */} {/* Email */}
<Box> <Box>
<Text size="sm" weight="medium" color="text-gray-300" block mb={2}> <Text size="sm" weight="medium" color="text-gray-300" block mb={2}>
Email Address Email Address
</Text> </Text>
<Box position="relative"> <Box position="relative">
<Box style={{ position: 'absolute', left: '0.75rem', top: '50%', transform: 'translateY(-50%)', zIndex: 10 }}> <Box position="absolute" left="3" top="50%" zIndex={10}>
<Icon icon={Mail} size={4} color="#6b7280" /> <Icon icon={Mail} size={4} color="#6b7280" />
</Box> </Box>
<Input <Input
@@ -77,7 +78,6 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
variant={viewData.formState.fields.email.error ? 'error' : 'default'} variant={viewData.formState.fields.email.error ? 'error' : 'default'}
placeholder="you@example.com" placeholder="you@example.com"
disabled={mutationState.isPending} disabled={mutationState.isPending}
style={{ paddingLeft: '2.5rem' }}
autoComplete="email" autoComplete="email"
/> />
</Box> </Box>
@@ -90,7 +90,7 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
{/* Error Message */} {/* Error Message */}
{mutationState.error && ( {mutationState.error && (
<Surface variant="muted" rounded="lg" border padding={3} style={{ backgroundColor: 'rgba(239, 68, 68, 0.1)', borderColor: 'rgba(239, 68, 68, 0.3)' }}> <Surface variant="muted" rounded="lg" border padding={3} bg="bg-red-500/10" borderColor="border-red-500/30">
<Stack direction="row" align="start" gap={3}> <Stack direction="row" align="start" gap={3}>
<Icon icon={AlertCircle} size={5} color="#ef4444" /> <Icon icon={AlertCircle} size={5} color="#ef4444" />
<Text size="sm" color="text-error-red">{mutationState.error}</Text> <Text size="sm" color="text-error-red">{mutationState.error}</Text>
@@ -110,8 +110,8 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
</Button> </Button>
{/* Back to Login */} {/* Back to Login */}
<Box style={{ textAlign: 'center' }}> <Box textAlign="center">
<Link href="/auth/login"> <Link href={routes.auth.login}>
<Stack direction="row" align="center" justify="center" gap={1}> <Stack direction="row" align="center" justify="center" gap={1}>
<Icon icon={ArrowLeft} size={4} color="#3b82f6" /> <Icon icon={ArrowLeft} size={4} color="#3b82f6" />
<Text size="sm" color="text-primary-blue">Back to Login</Text> <Text size="sm" color="text-primary-blue">Back to Login</Text>
@@ -119,10 +119,10 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
</Link> </Link>
</Box> </Box>
</Stack> </Stack>
</form> </Box>
) : ( ) : (
<Stack gap={4} style={{ position: 'relative' }}> <Stack gap={4} position="relative">
<Surface variant="muted" rounded="lg" border padding={4} style={{ backgroundColor: 'rgba(16, 185, 129, 0.1)', borderColor: 'rgba(16, 185, 129, 0.3)' }}> <Surface variant="muted" rounded="lg" border padding={4} bg="bg-green-500/10" borderColor="border-green-500/30">
<Stack direction="row" align="start" gap={3}> <Stack direction="row" align="start" gap={3}>
<Icon icon={CheckCircle2} size={6} color="#10b981" /> <Icon icon={CheckCircle2} size={6} color="#10b981" />
<Box> <Box>
@@ -130,8 +130,8 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
{viewData.magicLink && ( {viewData.magicLink && (
<Box mt={2}> <Box mt={2}>
<Text size="xs" color="text-gray-400" block mb={1}>Development Mode - Magic Link:</Text> <Text size="xs" color="text-gray-400" block mb={1}>Development Mode - Magic Link:</Text>
<Surface variant="muted" rounded="md" border padding={2} style={{ backgroundColor: '#262626' }}> <Surface variant="muted" rounded="md" border padding={2} bg="bg-neutral-800">
<Text size="xs" color="text-primary-blue" style={{ wordBreak: 'break-all' }}>{viewData.magicLink}</Text> <Text size="xs" color="text-primary-blue">{viewData.magicLink}</Text>
</Surface> </Surface>
<Text size="xs" color="text-gray-500" block mt={1}> <Text size="xs" color="text-gray-500" block mt={1}>
In production, this would be sent via email In production, this would be sent via email
@@ -167,7 +167,7 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
</Stack> </Stack>
{/* Footer */} {/* Footer */}
<Box mt={6} style={{ textAlign: 'center' }}> <Box mt={6} textAlign="center">
<Text size="xs" color="text-gray-500"> <Text size="xs" color="text-gray-500">
Need help?{' '} Need help?{' '}
<Link href="/support"> <Link href="/support">

View File

@@ -25,6 +25,7 @@ import { LoadingSpinner } from '@/ui/LoadingSpinner';
import { EnhancedFormError } from '@/components/errors/EnhancedFormError'; import { EnhancedFormError } from '@/components/errors/EnhancedFormError';
import { UserRolesPreview } from '@/components/auth/UserRolesPreview'; import { UserRolesPreview } from '@/components/auth/UserRolesPreview';
import { AuthWorkflowMockup } from '@/components/auth/AuthWorkflowMockup'; import { AuthWorkflowMockup } from '@/components/auth/AuthWorkflowMockup';
import { routes } from '@/lib/routing/RouteConfig';
import { LoginViewData } from '@/lib/builders/view-data/types/LoginViewData'; import { LoginViewData } from '@/lib/builders/view-data/types/LoginViewData';
import { FormState } from '@/lib/builders/view-data/types/FormState'; import { FormState } from '@/lib/builders/view-data/types/FormState';
@@ -45,16 +46,16 @@ interface LoginTemplateProps {
export function LoginTemplate({ viewData, formActions, mutationState }: LoginTemplateProps) { export function LoginTemplate({ viewData, formActions, mutationState }: LoginTemplateProps) {
return ( return (
<Box as="main" style={{ minHeight: '100vh', display: 'flex', position: 'relative' }}> <Box as="main" minHeight="100vh" display="flex" position="relative">
{/* Background Pattern */} {/* Background Pattern */}
<Box style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom right, rgba(59, 130, 246, 0.05), transparent, rgba(147, 51, 234, 0.05))' }} /> <Box position="absolute" inset="0" bg="linear-gradient(to bottom right, rgba(59, 130, 246, 0.05), transparent, rgba(147, 51, 234, 0.05))" />
{/* Left Side - Info Panel (Hidden on mobile) */} {/* Left Side - Info Panel (Hidden on mobile) */}
<Box className="hidden lg:flex lg:w-1/2" style={{ position: 'relative', alignItems: 'center', justifyContent: 'center', padding: '3rem' }}> <Box display={{ base: 'none', lg: 'flex' }} w={{ lg: '1/2' }} position="relative" alignItems="center" justifyContent="center" p={12}>
<Box style={{ maxWidth: '32rem' }}> <Box maxWidth="32rem">
{/* Logo */} {/* Logo */}
<Stack direction="row" align="center" gap={3} mb={8}> <Stack direction="row" align="center" gap={3} mb={8}>
<Surface variant="muted" rounded="xl" border padding={2} style={{ backgroundColor: 'rgba(59, 130, 246, 0.1)', borderColor: 'rgba(59, 130, 246, 0.3)' }}> <Surface variant="muted" rounded="xl" border padding={2} bg="bg-blue-500/10" borderColor="border-blue-500/30">
<Icon icon={Flag} size={6} color="#3b82f6" /> <Icon icon={Flag} size={6} color="#3b82f6" />
</Surface> </Surface>
<Text size="2xl" weight="bold" color="text-white">GridPilot</Text> <Text size="2xl" weight="bold" color="text-white">GridPilot</Text>
@@ -90,11 +91,11 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
</Box> </Box>
{/* Right Side - Login Form */} {/* Right Side - Login Form */}
<Box style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '3rem 1rem', position: 'relative' }}> <Box flex={1} display="flex" alignItems="center" justifyContent="center" p={{ base: 4, lg: 12 }} position="relative">
<Box style={{ width: '100%', maxWidth: '28rem' }}> <Box w="full" maxWidth="28rem">
{/* Mobile Logo/Header */} {/* Mobile Logo/Header */}
<Box className="lg:hidden" style={{ textAlign: 'center' }} mb={8}> <Box display={{ base: 'block', lg: 'none' }} textAlign="center" mb={8}>
<Surface variant="muted" rounded="2xl" border padding={4} style={{ width: '4rem', height: '4rem', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 1rem' }}> <Surface variant="muted" rounded="2xl" border padding={4} w="4rem" h="4rem" display="flex" alignItems="center" justifyContent="center" mx="auto" mb={4}>
<Icon icon={Flag} size={8} color="#3b82f6" /> <Icon icon={Flag} size={8} color="#3b82f6" />
</Surface> </Surface>
<Heading level={1}>Welcome Back</Heading> <Heading level={1}>Welcome Back</Heading>
@@ -104,26 +105,26 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
</Box> </Box>
{/* Desktop Header */} {/* Desktop Header */}
<Box className="hidden lg:block" style={{ textAlign: 'center' }} mb={8}> <Box display={{ base: 'none', lg: 'block' }} textAlign="center" mb={8}>
<Heading level={2}>Welcome Back</Heading> <Heading level={2}>Welcome Back</Heading>
<Text color="text-gray-400" block mt={2}> <Text color="text-gray-400" block mt={2}>
Sign in to access your racing dashboard Sign in to access your racing dashboard
</Text> </Text>
</Box> </Box>
<Card style={{ position: 'relative', overflow: 'hidden' }}> <Card position="relative" overflow="hidden">
{/* Background accent */} {/* Background accent */}
<Box style={{ position: 'absolute', top: 0, right: 0, width: '8rem', height: '8rem', background: 'linear-gradient(to bottom left, rgba(59, 130, 246, 0.1), transparent)', borderBottomLeftRadius: '9999px' }} /> <Box position="absolute" top="0" right="0" w="8rem" h="8rem" bg="linear-gradient(to bottom left, rgba(59, 130, 246, 0.1), transparent)" />
<form onSubmit={formActions.handleSubmit}> <Box as="form" onSubmit={formActions.handleSubmit}>
<Stack gap={5} style={{ position: 'relative' }}> <Stack gap={5} position="relative">
{/* Email */} {/* Email */}
<Box> <Box>
<Text size="sm" weight="medium" color="text-gray-300" block mb={2}> <Text size="sm" weight="medium" color="text-gray-300" block mb={2}>
Email Address Email Address
</Text> </Text>
<Box position="relative"> <Box position="relative">
<Box style={{ position: 'absolute', left: '0.75rem', top: '50%', transform: 'translateY(-50%)', zIndex: 10 }}> <Box position="absolute" left="3" top="50%" zIndex={10}>
<Icon icon={Mail} size={4} color="#6b7280" /> <Icon icon={Mail} size={4} color="#6b7280" />
</Box> </Box>
<Input <Input
@@ -135,7 +136,6 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
variant={viewData.formState.fields.email.error ? 'error' : 'default'} variant={viewData.formState.fields.email.error ? 'error' : 'default'}
placeholder="you@example.com" placeholder="you@example.com"
disabled={viewData.formState.isSubmitting || mutationState.isPending} disabled={viewData.formState.isSubmitting || mutationState.isPending}
style={{ paddingLeft: '2.5rem' }}
autoComplete="email" autoComplete="email"
/> />
</Box> </Box>
@@ -152,12 +152,12 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
<Text size="sm" weight="medium" color="text-gray-300"> <Text size="sm" weight="medium" color="text-gray-300">
Password Password
</Text> </Text>
<Link href="/auth/forgot-password"> <Link href={routes.auth.forgotPassword}>
<Text size="xs" color="text-primary-blue">Forgot password?</Text> <Text size="xs" color="text-primary-blue">Forgot password?</Text>
</Link> </Link>
</Stack> </Stack>
<Box position="relative"> <Box position="relative">
<Box style={{ position: 'absolute', left: '0.75rem', top: '50%', transform: 'translateY(-50%)', zIndex: 10 }}> <Box position="absolute" left="3" top="50%" zIndex={10}>
<Icon icon={Lock} size={4} color="#6b7280" /> <Icon icon={Lock} size={4} color="#6b7280" />
</Box> </Box>
<Input <Input
@@ -169,14 +169,19 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
variant={viewData.formState.fields.password.error ? 'error' : 'default'} variant={viewData.formState.fields.password.error ? 'error' : 'default'}
placeholder="••••••••" placeholder="••••••••"
disabled={viewData.formState.isSubmitting || mutationState.isPending} disabled={viewData.formState.isSubmitting || mutationState.isPending}
style={{ paddingLeft: '2.5rem', paddingRight: '2.5rem' }}
autoComplete="current-password" autoComplete="current-password"
/> />
<Box <Box
as="button" as="button"
type="button" type="button"
onClick={() => formActions.setShowPassword(!viewData.showPassword)} onClick={() => formActions.setShowPassword(!viewData.showPassword)}
style={{ position: 'absolute', right: '0.75rem', top: '50%', transform: 'translateY(-50%)', zIndex: 10, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }} position="absolute"
right="3"
top="50%"
zIndex={10}
bg="transparent"
borderStyle="none"
cursor="pointer"
> >
<Icon icon={viewData.showPassword ? EyeOff : Eye} size={4} color="#6b7280" /> <Icon icon={viewData.showPassword ? EyeOff : Eye} size={4} color="#6b7280" />
</Box> </Box>
@@ -190,27 +195,27 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
{/* Remember Me */} {/* Remember Me */}
<Stack direction="row" align="center" gap={2}> <Stack direction="row" align="center" gap={2}>
<input <Box
as="input"
id="rememberMe" id="rememberMe"
name="rememberMe" name="rememberMe"
type="checkbox" type="checkbox"
checked={viewData.formState.fields.rememberMe.value as boolean} checked={viewData.formState.fields.rememberMe.value as boolean}
onChange={formActions.handleChange} onChange={formActions.handleChange}
disabled={viewData.formState.isSubmitting || mutationState.isPending} disabled={viewData.formState.isSubmitting || mutationState.isPending}
className="w-4 h-4 rounded border-charcoal-outline bg-iron-gray text-primary-blue focus:ring-primary-blue focus:ring-offset-0"
/> />
<Text size="sm" color="text-gray-300">Keep me signed in</Text> <Text size="sm" color="text-gray-300">Keep me signed in</Text>
</Stack> </Stack>
{/* Insufficient Permissions Message */} {/* Insufficient Permissions Message */}
{viewData.hasInsufficientPermissions && ( {viewData.hasInsufficientPermissions && (
<Surface variant="muted" rounded="lg" border padding={4} style={{ backgroundColor: 'rgba(245, 158, 11, 0.1)', borderColor: 'rgba(245, 158, 11, 0.3)' }}> <Surface variant="muted" rounded="lg" border padding={4} bg="bg-amber-500/10" borderColor="border-amber-500/30">
<Stack direction="row" align="start" gap={3}> <Stack direction="row" align="start" gap={3}>
<Icon icon={AlertCircle} size={5} color="#f59e0b" /> <Icon icon={AlertCircle} size={5} color="#f59e0b" />
<Box> <Box>
<Text weight="bold" color="text-warning-amber" block>Insufficient Permissions</Text> <Text weight="bold" color="text-warning-amber" block>Insufficient Permissions</Text>
<Text size="sm" color="text-gray-300" block mt={1}> <Text size="sm" color="text-gray-300" block mt={1}>
You don't have permission to access that page. Please log in with an account that has the required role. You don&apos;t have permission to access that page. Please log in with an account that has the required role.
</Text> </Text>
</Box> </Box>
</Stack> </Stack>
@@ -239,24 +244,24 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
{mutationState.isPending || viewData.formState.isSubmitting ? 'Signing in...' : 'Sign In'} {mutationState.isPending || viewData.formState.isSubmitting ? 'Signing in...' : 'Sign In'}
</Button> </Button>
</Stack> </Stack>
</form> </Box>
{/* Divider */} {/* Divider */}
<Box style={{ position: 'relative' }} my={6}> <Box position="relative" my={6}>
<Box style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center' }}> <Box position="absolute" inset="0" display="flex" alignItems="center">
<Box style={{ width: '100%', borderTop: '1px solid #262626' }} /> <Box w="full" borderTop borderColor="border-neutral-800" />
</Box> </Box>
<Box style={{ position: 'relative', display: 'flex', justifyContent: 'center' }}> <Box position="relative" display="flex" justifyContent="center">
<Box px={4} style={{ backgroundColor: '#171717' }}> <Box px={4} bg="bg-neutral-900">
<Text size="xs" color="text-gray-500">or continue with</Text> <Text size="xs" color="text-gray-500">or continue with</Text>
</Box> </Box>
</Box> </Box>
</Box> </Box>
{/* Sign Up Link */} {/* Sign Up Link */}
<Box style={{ textAlign: 'center' }} mt={6}> <Box textAlign="center" mt={6}>
<Text size="sm" color="text-gray-400"> <Text size="sm" color="text-gray-400">
Don't have an account?{' '} Don&apos;t have an account?{' '}
<Link <Link
href={viewData.returnTo && viewData.returnTo !== '/dashboard' ? `/auth/signup?returnTo=${encodeURIComponent(viewData.returnTo)}` : '/auth/signup'} href={viewData.returnTo && viewData.returnTo !== '/dashboard' ? `/auth/signup?returnTo=${encodeURIComponent(viewData.returnTo)}` : '/auth/signup'}
> >
@@ -268,18 +273,18 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
{/* Name Immutability Notice */} {/* Name Immutability Notice */}
<Box mt={6}> <Box mt={6}>
<Surface variant="muted" rounded="lg" border padding={4} style={{ backgroundColor: 'rgba(38, 38, 38, 0.3)', borderColor: '#262626' }}> <Surface variant="muted" rounded="lg" border padding={4} bg="bg-neutral-800/30" borderColor="border-neutral-800">
<Stack direction="row" align="start" gap={3}> <Stack direction="row" align="start" gap={3}>
<Icon icon={AlertCircle} size={5} color="#737373" /> <Icon icon={AlertCircle} size={5} color="#737373" />
<Text size="xs" color="text-gray-400"> <Text size="xs" color="text-gray-400">
<Text weight="bold">Note:</Text> Your display name cannot be changed after signup. Please ensure it's correct when creating your account. <Text weight="bold">Note:</Text> Your display name cannot be changed after signup. Please ensure it&apos;s correct when creating your account.
</Text> </Text>
</Stack> </Stack>
</Surface> </Surface>
</Box> </Box>
{/* Footer */} {/* Footer */}
<Box mt={6} style={{ textAlign: 'center' }}> <Box mt={6} textAlign="center">
<Text size="xs" color="text-gray-500"> <Text size="xs" color="text-gray-500">
By signing in, you agree to our{' '} By signing in, you agree to our{' '}
<Link href="/terms"> <Link href="/terms">
@@ -293,7 +298,7 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
</Box> </Box>
{/* Mobile Role Info */} {/* Mobile Role Info */}
<Box mt={8} className="lg:hidden"> <Box mt={8} display={{ base: 'block', lg: 'none' }}>
<UserRolesPreview variant="compact" /> <UserRolesPreview variant="compact" />
</Box> </Box>
</Box> </Box>

View File

@@ -22,9 +22,11 @@ import { Link } from '@/ui/Link';
import { Surface } from '@/ui/Surface'; import { Surface } from '@/ui/Surface';
import { Icon } from '@/ui/Icon'; import { Icon } from '@/ui/Icon';
import { LoadingSpinner } from '@/ui/LoadingSpinner'; import { LoadingSpinner } from '@/ui/LoadingSpinner';
import { routes } from '@/lib/routing/RouteConfig';
import { ResetPasswordViewData } from '@/lib/builders/view-data/types/ResetPasswordViewData'; import { ResetPasswordViewData } from '@/lib/builders/view-data/types/ResetPasswordViewData';
interface ResetPasswordTemplateProps extends ResetPasswordViewData { interface ResetPasswordTemplateProps {
viewData: ResetPasswordViewData;
formActions: { formActions: {
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void; handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => Promise<void>; handleSubmit: (e: React.FormEvent<HTMLFormElement>) => Promise<void>;
@@ -42,18 +44,21 @@ interface ResetPasswordTemplateProps extends ResetPasswordViewData {
}; };
} }
export function ResetPasswordTemplate(props: ResetPasswordTemplateProps) { export function ResetPasswordTemplate({
const { formActions, uiState, mutationState, ...viewData } = props; viewData,
formActions,
uiState,
mutationState,
}: ResetPasswordTemplateProps) {
return ( return (
<Box as="main" style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative' }}> <Box as="main" minHeight="100vh" display="flex" alignItems="center" justifyContent="center" position="relative">
{/* Background Pattern */} {/* Background Pattern */}
<Box style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom right, rgba(59, 130, 246, 0.05), transparent, rgba(147, 51, 234, 0.05))' }} /> <Box position="absolute" inset="0" bg="linear-gradient(to bottom right, rgba(59, 130, 246, 0.05), transparent, rgba(147, 51, 234, 0.05))" />
<Box style={{ position: 'relative', width: '100%', maxWidth: '28rem', padding: '0 1rem' }}> <Box position="relative" w="full" maxWidth="28rem" px={4}>
{/* Header */} {/* Header */}
<Box style={{ textAlign: 'center' }} mb={8}> <Box textAlign="center" mb={8}>
<Surface variant="muted" rounded="2xl" border padding={4} style={{ width: '4rem', height: '4rem', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 1rem' }}> <Surface variant="muted" rounded="2xl" border padding={4} w="4rem" h="4rem" display="flex" alignItems="center" justifyContent="center" mx="auto" mb={4}>
<Icon icon={Flag} size={8} color="#3b82f6" /> <Icon icon={Flag} size={8} color="#3b82f6" />
</Surface> </Surface>
<Heading level={1}>Reset Password</Heading> <Heading level={1}>Reset Password</Heading>
@@ -62,20 +67,20 @@ export function ResetPasswordTemplate(props: ResetPasswordTemplateProps) {
</Text> </Text>
</Box> </Box>
<Card style={{ position: 'relative', overflow: 'hidden' }}> <Card position="relative" overflow="hidden">
{/* Background accent */} {/* Background accent */}
<Box style={{ position: 'absolute', top: 0, right: 0, width: '8rem', height: '8rem', background: 'linear-gradient(to bottom left, rgba(59, 130, 246, 0.1), transparent)', borderBottomLeftRadius: '9999px' }} /> <Box position="absolute" top="0" right="0" w="8rem" h="8rem" bg="linear-gradient(to bottom left, rgba(59, 130, 246, 0.1), transparent)" />
{!viewData.showSuccess ? ( {!viewData.showSuccess ? (
<form onSubmit={formActions.handleSubmit}> <Box as="form" onSubmit={formActions.handleSubmit}>
<Stack gap={5} style={{ position: 'relative' }}> <Stack gap={5} position="relative">
{/* New Password */} {/* New Password */}
<Box> <Box>
<Text size="sm" weight="medium" color="text-gray-300" block mb={2}> <Text size="sm" weight="medium" color="text-gray-300" block mb={2}>
New Password New Password
</Text> </Text>
<Box position="relative"> <Box position="relative">
<Box style={{ position: 'absolute', left: '0.75rem', top: '50%', transform: 'translateY(-50%)', zIndex: 10 }}> <Box position="absolute" left="3" top="50%" zIndex={10}>
<Icon icon={Lock} size={4} color="#6b7280" /> <Icon icon={Lock} size={4} color="#6b7280" />
</Box> </Box>
<Input <Input
@@ -87,14 +92,19 @@ export function ResetPasswordTemplate(props: ResetPasswordTemplateProps) {
variant={viewData.formState.fields.newPassword.error ? 'error' : 'default'} variant={viewData.formState.fields.newPassword.error ? 'error' : 'default'}
placeholder="••••••••" placeholder="••••••••"
disabled={mutationState.isPending} disabled={mutationState.isPending}
style={{ paddingLeft: '2.5rem', paddingRight: '2.5rem' }}
autoComplete="new-password" autoComplete="new-password"
/> />
<Box <Box
as="button" as="button"
type="button" type="button"
onClick={() => formActions.setShowPassword(!uiState.showPassword)} onClick={() => formActions.setShowPassword(!uiState.showPassword)}
style={{ position: 'absolute', right: '0.75rem', top: '50%', transform: 'translateY(-50%)', zIndex: 10, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }} position="absolute"
right="3"
top="50%"
zIndex={10}
bg="transparent"
borderStyle="none"
cursor="pointer"
> >
<Icon icon={uiState.showPassword ? EyeOff : Eye} size={4} color="#6b7280" /> <Icon icon={uiState.showPassword ? EyeOff : Eye} size={4} color="#6b7280" />
</Box> </Box>
@@ -112,7 +122,7 @@ export function ResetPasswordTemplate(props: ResetPasswordTemplateProps) {
Confirm Password Confirm Password
</Text> </Text>
<Box position="relative"> <Box position="relative">
<Box style={{ position: 'absolute', left: '0.75rem', top: '50%', transform: 'translateY(-50%)', zIndex: 10 }}> <Box position="absolute" left="3" top="50%" zIndex={10}>
<Icon icon={Lock} size={4} color="#6b7280" /> <Icon icon={Lock} size={4} color="#6b7280" />
</Box> </Box>
<Input <Input
@@ -124,14 +134,19 @@ export function ResetPasswordTemplate(props: ResetPasswordTemplateProps) {
variant={viewData.formState.fields.confirmPassword.error ? 'error' : 'default'} variant={viewData.formState.fields.confirmPassword.error ? 'error' : 'default'}
placeholder="••••••••" placeholder="••••••••"
disabled={mutationState.isPending} disabled={mutationState.isPending}
style={{ paddingLeft: '2.5rem', paddingRight: '2.5rem' }}
autoComplete="new-password" autoComplete="new-password"
/> />
<Box <Box
as="button" as="button"
type="button" type="button"
onClick={() => formActions.setShowConfirmPassword(!uiState.showConfirmPassword)} onClick={() => formActions.setShowConfirmPassword(!uiState.showConfirmPassword)}
style={{ position: 'absolute', right: '0.75rem', top: '50%', transform: 'translateY(-50%)', zIndex: 10, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }} position="absolute"
right="3"
top="50%"
zIndex={10}
bg="transparent"
borderStyle="none"
cursor="pointer"
> >
<Icon icon={uiState.showConfirmPassword ? EyeOff : Eye} size={4} color="#6b7280" /> <Icon icon={uiState.showConfirmPassword ? EyeOff : Eye} size={4} color="#6b7280" />
</Box> </Box>
@@ -145,7 +160,7 @@ export function ResetPasswordTemplate(props: ResetPasswordTemplateProps) {
{/* Error Message */} {/* Error Message */}
{mutationState.error && ( {mutationState.error && (
<Surface variant="muted" rounded="lg" border padding={3} style={{ backgroundColor: 'rgba(239, 68, 68, 0.1)', borderColor: 'rgba(239, 68, 68, 0.3)' }}> <Surface variant="muted" rounded="lg" border padding={3} bg="bg-red-500/10" borderColor="border-red-500/30">
<Stack direction="row" align="start" gap={3}> <Stack direction="row" align="start" gap={3}>
<Icon icon={AlertCircle} size={5} color="#ef4444" /> <Icon icon={AlertCircle} size={5} color="#ef4444" />
<Text size="sm" color="text-error-red">{mutationState.error}</Text> <Text size="sm" color="text-error-red">{mutationState.error}</Text>
@@ -165,8 +180,8 @@ export function ResetPasswordTemplate(props: ResetPasswordTemplateProps) {
</Button> </Button>
{/* Back to Login */} {/* Back to Login */}
<Box style={{ textAlign: 'center' }}> <Box textAlign="center">
<Link href="/auth/login"> <Link href={routes.auth.login}>
<Stack direction="row" align="center" justify="center" gap={1}> <Stack direction="row" align="center" justify="center" gap={1}>
<Icon icon={ArrowLeft} size={4} color="#3b82f6" /> <Icon icon={ArrowLeft} size={4} color="#3b82f6" />
<Text size="sm" color="text-primary-blue">Back to Login</Text> <Text size="sm" color="text-primary-blue">Back to Login</Text>
@@ -174,10 +189,10 @@ export function ResetPasswordTemplate(props: ResetPasswordTemplateProps) {
</Link> </Link>
</Box> </Box>
</Stack> </Stack>
</form> </Box>
) : ( ) : (
<Stack gap={4} style={{ position: 'relative' }}> <Stack gap={4} position="relative">
<Surface variant="muted" rounded="lg" border padding={4} style={{ backgroundColor: 'rgba(16, 185, 129, 0.1)', borderColor: 'rgba(16, 185, 129, 0.3)' }}> <Surface variant="muted" rounded="lg" border padding={4} bg="bg-green-500/10" borderColor="border-green-500/30">
<Stack direction="row" align="start" gap={3}> <Stack direction="row" align="start" gap={3}>
<Icon icon={CheckCircle2} size={6} color="#10b981" /> <Icon icon={CheckCircle2} size={6} color="#10b981" />
<Box> <Box>
@@ -214,7 +229,7 @@ export function ResetPasswordTemplate(props: ResetPasswordTemplateProps) {
</Stack> </Stack>
{/* Footer */} {/* Footer */}
<Box mt={6} style={{ textAlign: 'center' }}> <Box mt={6} textAlign="center">
<Text size="xs" color="text-gray-500"> <Text size="xs" color="text-gray-500">
Need help?{' '} Need help?{' '}
<Link href="/support"> <Link href="/support">

View File

@@ -56,18 +56,21 @@ const USER_ROLES = [
title: 'Driver', title: 'Driver',
description: 'Race, track stats, join teams', description: 'Race, track stats, join teams',
color: '#3b82f6', color: '#3b82f6',
bg: 'bg-blue-500/10',
}, },
{ {
icon: Trophy, icon: Trophy,
title: 'League Admin', title: 'League Admin',
description: 'Organize leagues and events', description: 'Organize leagues and events',
color: '#10b981', color: '#10b981',
bg: 'bg-green-500/10',
}, },
{ {
icon: Users, icon: Users,
title: 'Team Manager', title: 'Team Manager',
description: 'Manage team and drivers', description: 'Manage team and drivers',
color: '#a855f7', color: '#a855f7',
bg: 'bg-purple-500/10',
}, },
]; ];
@@ -91,16 +94,16 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
]; ];
return ( return (
<Box as="main" style={{ minHeight: '100vh', display: 'flex', position: 'relative' }}> <Box as="main" minHeight="100vh" display="flex" position="relative">
{/* Background Pattern */} {/* Background Pattern */}
<Box style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom right, rgba(59, 130, 246, 0.05), transparent, rgba(147, 51, 234, 0.05))' }} /> <Box position="absolute" inset="0" bg="linear-gradient(to bottom right, rgba(59, 130, 246, 0.05), transparent, rgba(147, 51, 234, 0.05))" />
{/* Left Side - Info Panel (Hidden on mobile) */} {/* Left Side - Info Panel (Hidden on mobile) */}
<Box className="hidden lg:flex lg:w-1/2" style={{ position: 'relative', alignItems: 'center', justifyContent: 'center', padding: '3rem' }}> <Box display={{ base: 'none', lg: 'flex' }} w={{ lg: '1/2' }} position="relative" alignItems="center" justifyContent="center" p={12}>
<Box style={{ maxWidth: '32rem' }}> <Box maxWidth="32rem">
{/* Logo */} {/* Logo */}
<Stack direction="row" align="center" gap={3} mb={8}> <Stack direction="row" align="center" gap={3} mb={8}>
<Surface variant="muted" rounded="xl" border padding={2} style={{ backgroundColor: 'rgba(59, 130, 246, 0.1)', borderColor: 'rgba(59, 130, 246, 0.3)' }}> <Surface variant="muted" rounded="xl" border padding={2} bg="bg-blue-500/10" borderColor="border-blue-500/30">
<Icon icon={Flag} size={6} color="#3b82f6" /> <Icon icon={Flag} size={6} color="#3b82f6" />
</Surface> </Surface>
<Text size="2xl" weight="bold" color="text-white">GridPilot</Text> <Text size="2xl" weight="bold" color="text-white">GridPilot</Text>
@@ -123,10 +126,11 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
rounded="xl" rounded="xl"
border border
padding={4} padding={4}
style={{ backgroundColor: 'rgba(38, 38, 38, 0.3)', borderColor: '#262626' }} bg="bg-neutral-800/30"
borderColor="border-neutral-800"
> >
<Stack direction="row" align="center" gap={4}> <Stack direction="row" align="center" gap={4}>
<Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: `${role.color}1A` }}> <Surface variant="muted" rounded="lg" padding={2} bg={role.bg}>
<Icon icon={role.icon} size={5} color={role.color} /> <Icon icon={role.icon} size={5} color={role.color} />
</Surface> </Surface>
<Box> <Box>
@@ -140,7 +144,7 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
{/* Features List */} {/* Features List */}
<Box mb={8}> <Box mb={8}>
<Surface variant="muted" rounded="xl" border padding={5} style={{ backgroundColor: 'rgba(38, 38, 38, 0.2)', borderColor: '#262626' }}> <Surface variant="muted" rounded="xl" border padding={5} bg="bg-neutral-800/20" borderColor="border-neutral-800">
<Stack direction="row" align="center" gap={2} mb={4}> <Stack direction="row" align="center" gap={2} mb={4}>
<Icon icon={Sparkles} size={4} color="#3b82f6" /> <Icon icon={Sparkles} size={4} color="#3b82f6" />
<Text size="sm" weight="medium" color="text-white">What you&apos;ll get</Text> <Text size="sm" weight="medium" color="text-white">What you&apos;ll get</Text>
@@ -168,11 +172,11 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
</Box> </Box>
{/* Right Side - Signup Form */} {/* Right Side - Signup Form */}
<Box style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '3rem 1rem', position: 'relative', overflowY: 'auto' }}> <Box flex={1} display="flex" alignItems="center" justifyContent="center" p={{ base: 4, lg: 12 }} position="relative" overflow="auto">
<Box style={{ width: '100%', maxWidth: '28rem' }}> <Box w="full" maxWidth="28rem">
{/* Mobile Logo/Header */} {/* Mobile Logo/Header */}
<Box className="lg:hidden" style={{ textAlign: 'center' }} mb={8}> <Box display={{ base: 'block', lg: 'none' }} textAlign="center" mb={8}>
<Surface variant="muted" rounded="2xl" border padding={4} style={{ width: '4rem', height: '4rem', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 1rem' }}> <Surface variant="muted" rounded="2xl" border padding={4} w="4rem" h="4rem" display="flex" alignItems="center" justifyContent="center" mx="auto" mb={4}>
<Icon icon={Flag} size={8} color="#3b82f6" /> <Icon icon={Flag} size={8} color="#3b82f6" />
</Surface> </Surface>
<Heading level={1}>Join GridPilot</Heading> <Heading level={1}>Join GridPilot</Heading>
@@ -182,26 +186,26 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
</Box> </Box>
{/* Desktop Header */} {/* Desktop Header */}
<Box className="hidden lg:block" style={{ textAlign: 'center' }} mb={8}> <Box display={{ base: 'none', lg: 'block' }} textAlign="center" mb={8}>
<Heading level={2}>Create Account</Heading> <Heading level={2}>Create Account</Heading>
<Text color="text-gray-400" block mt={2}> <Text color="text-gray-400" block mt={2}>
Get started with your free account Get started with your free account
</Text> </Text>
</Box> </Box>
<Card style={{ position: 'relative', overflow: 'hidden' }}> <Card position="relative" overflow="hidden">
{/* Background accent */} {/* Background accent */}
<Box style={{ position: 'absolute', top: 0, right: 0, width: '8rem', height: '8rem', background: 'linear-gradient(to bottom left, rgba(59, 130, 246, 0.1), transparent)', borderBottomLeftRadius: '9999px' }} /> <Box position="absolute" top="0" right="0" w="8rem" h="8rem" bg="linear-gradient(to bottom left, rgba(59, 130, 246, 0.1), transparent)" />
<form onSubmit={formActions.handleSubmit}> <Box as="form" onSubmit={formActions.handleSubmit}>
<Stack gap={4} style={{ position: 'relative' }}> <Stack gap={4} position="relative">
{/* First Name */} {/* First Name */}
<Box> <Box>
<Text size="sm" weight="medium" color="text-gray-300" block mb={2}> <Text size="sm" weight="medium" color="text-gray-300" block mb={2}>
First Name First Name
</Text> </Text>
<Box position="relative"> <Box position="relative">
<Box style={{ position: 'absolute', left: '0.75rem', top: '50%', transform: 'translateY(-50%)', zIndex: 10 }}> <Box position="absolute" left="3" top="50%" zIndex={10}>
<Icon icon={User} size={4} color="#6b7280" /> <Icon icon={User} size={4} color="#6b7280" />
</Box> </Box>
<Input <Input
@@ -213,7 +217,6 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
variant={viewData.formState.fields.firstName.error ? 'error' : 'default'} variant={viewData.formState.fields.firstName.error ? 'error' : 'default'}
placeholder="John" placeholder="John"
disabled={mutationState.isPending} disabled={mutationState.isPending}
style={{ paddingLeft: '2.5rem' }}
autoComplete="given-name" autoComplete="given-name"
/> />
</Box> </Box>
@@ -230,7 +233,7 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
Last Name Last Name
</Text> </Text>
<Box position="relative"> <Box position="relative">
<Box style={{ position: 'absolute', left: '0.75rem', top: '50%', transform: 'translateY(-50%)', zIndex: 10 }}> <Box position="absolute" left="3" top="50%" zIndex={10}>
<Icon icon={User} size={4} color="#6b7280" /> <Icon icon={User} size={4} color="#6b7280" />
</Box> </Box>
<Input <Input
@@ -242,7 +245,6 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
variant={viewData.formState.fields.lastName.error ? 'error' : 'default'} variant={viewData.formState.fields.lastName.error ? 'error' : 'default'}
placeholder="Smith" placeholder="Smith"
disabled={mutationState.isPending} disabled={mutationState.isPending}
style={{ paddingLeft: '2.5rem' }}
autoComplete="family-name" autoComplete="family-name"
/> />
</Box> </Box>
@@ -255,11 +257,11 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
</Box> </Box>
{/* Name Immutability Warning */} {/* Name Immutability Warning */}
<Surface variant="muted" rounded="lg" border padding={3} style={{ backgroundColor: 'rgba(245, 158, 11, 0.1)', borderColor: 'rgba(245, 158, 11, 0.3)' }}> <Surface variant="muted" rounded="lg" border padding={3} bg="bg-amber-500/10" borderColor="border-amber-500/30">
<Stack direction="row" align="start" gap={3}> <Stack direction="row" align="start" gap={3}>
<Icon icon={AlertCircle} size={5} color="#f59e0b" /> <Icon icon={AlertCircle} size={5} color="#f59e0b" />
<Text size="sm" color="text-warning-amber"> <Text size="sm" color="text-warning-amber">
<Text weight="bold">Important:</Text> Your name cannot be changed after signup. Please ensure it's correct. <Text weight="bold">Important:</Text> Your name cannot be changed after signup. Please ensure it&apos;s correct.
</Text> </Text>
</Stack> </Stack>
</Surface> </Surface>
@@ -270,7 +272,7 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
Email Address Email Address
</Text> </Text>
<Box position="relative"> <Box position="relative">
<Box style={{ position: 'absolute', left: '0.75rem', top: '50%', transform: 'translateY(-50%)', zIndex: 10 }}> <Box position="absolute" left="3" top="50%" zIndex={10}>
<Icon icon={Mail} size={4} color="#6b7280" /> <Icon icon={Mail} size={4} color="#6b7280" />
</Box> </Box>
<Input <Input
@@ -282,7 +284,6 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
variant={viewData.formState.fields.email.error ? 'error' : 'default'} variant={viewData.formState.fields.email.error ? 'error' : 'default'}
placeholder="you@example.com" placeholder="you@example.com"
disabled={mutationState.isPending} disabled={mutationState.isPending}
style={{ paddingLeft: '2.5rem' }}
autoComplete="email" autoComplete="email"
/> />
</Box> </Box>
@@ -299,7 +300,7 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
Password Password
</Text> </Text>
<Box position="relative"> <Box position="relative">
<Box style={{ position: 'absolute', left: '0.75rem', top: '50%', transform: 'translateY(-50%)', zIndex: 10 }}> <Box position="absolute" left="3" top="50%" zIndex={10}>
<Icon icon={Lock} size={4} color="#6b7280" /> <Icon icon={Lock} size={4} color="#6b7280" />
</Box> </Box>
<Input <Input
@@ -311,14 +312,19 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
variant={viewData.formState.fields.password.error ? 'error' : 'default'} variant={viewData.formState.fields.password.error ? 'error' : 'default'}
placeholder="••••••••" placeholder="••••••••"
disabled={mutationState.isPending} disabled={mutationState.isPending}
style={{ paddingLeft: '2.5rem', paddingRight: '2.5rem' }}
autoComplete="new-password" autoComplete="new-password"
/> />
<Box <Box
as="button" as="button"
type="button" type="button"
onClick={() => formActions.setShowPassword(!uiState.showPassword)} onClick={() => formActions.setShowPassword(!uiState.showPassword)}
style={{ position: 'absolute', right: '0.75rem', top: '50%', transform: 'translateY(-50%)', zIndex: 10, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }} position="absolute"
right="3"
top="50%"
zIndex={10}
bg="transparent"
borderStyle="none"
cursor="pointer"
> >
<Icon icon={uiState.showPassword ? EyeOff : Eye} size={4} color="#6b7280" /> <Icon icon={uiState.showPassword ? EyeOff : Eye} size={4} color="#6b7280" />
</Box> </Box>
@@ -333,14 +339,14 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
{viewData.formState.fields.password.value && ( {viewData.formState.fields.password.value && (
<Box mt={3}> <Box mt={3}>
<Stack direction="row" align="center" gap={2} mb={2}> <Stack direction="row" align="center" gap={2} mb={2}>
<Box style={{ flex: 1, height: '0.375rem', borderRadius: '9999px', backgroundColor: '#262626', overflow: 'hidden' }}> <Box flex={1} h="1.5" rounded="full" bg="bg-neutral-800" overflow="hidden">
<Box style={{ height: '100%', width: `${(passwordStrength.score / 5) * 100}%`, backgroundColor: passwordStrength.color === 'bg-red-500' ? '#ef4444' : passwordStrength.color === 'bg-yellow-500' ? '#f59e0b' : passwordStrength.color === 'bg-blue-500' ? '#3b82f6' : '#10b981' }} /> <Box h="full" w={`${(passwordStrength.score / 5) * 100}%`} bg={passwordStrength.color === 'bg-red-500' ? 'bg-red-500' : passwordStrength.color === 'bg-yellow-500' ? 'bg-amber-500' : passwordStrength.color === 'bg-blue-500' ? 'bg-blue-500' : 'bg-green-500'} />
</Box> </Box>
<Text size="xs" weight="medium" style={{ color: passwordStrength.color === 'bg-red-500' ? '#f87171' : passwordStrength.color === 'bg-yellow-500' ? '#fbbf24' : passwordStrength.color === 'bg-blue-500' ? '#60a5fa' : '#34d399' }}> <Text size="xs" weight="medium" color={passwordStrength.color === 'bg-red-500' ? 'text-red-400' : passwordStrength.color === 'bg-yellow-500' ? 'text-amber-400' : passwordStrength.color === 'bg-blue-500' ? 'text-blue-400' : 'text-green-400'}>
{passwordStrength.label} {passwordStrength.label}
</Text> </Text>
</Stack> </Stack>
<Box style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '0.25rem' }}> <Box display="grid" gridCols={2} gap={1}>
{passwordRequirements.map((req, index) => ( {passwordRequirements.map((req, index) => (
<Stack key={index} direction="row" align="center" gap={1.5}> <Stack key={index} direction="row" align="center" gap={1.5}>
<Icon icon={req.met ? Check : X} size={3} color={req.met ? '#10b981' : '#525252'} /> <Icon icon={req.met ? Check : X} size={3} color={req.met ? '#10b981' : '#525252'} />
@@ -360,7 +366,7 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
Confirm Password Confirm Password
</Text> </Text>
<Box position="relative"> <Box position="relative">
<Box style={{ position: 'absolute', left: '0.75rem', top: '50%', transform: 'translateY(-50%)', zIndex: 10 }}> <Box position="absolute" left="3" top="50%" zIndex={10}>
<Icon icon={Lock} size={4} color="#6b7280" /> <Icon icon={Lock} size={4} color="#6b7280" />
</Box> </Box>
<Input <Input
@@ -372,14 +378,19 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
variant={viewData.formState.fields.confirmPassword.error ? 'error' : 'default'} variant={viewData.formState.fields.confirmPassword.error ? 'error' : 'default'}
placeholder="••••••••" placeholder="••••••••"
disabled={mutationState.isPending} disabled={mutationState.isPending}
style={{ paddingLeft: '2.5rem', paddingRight: '2.5rem' }}
autoComplete="new-password" autoComplete="new-password"
/> />
<Box <Box
as="button" as="button"
type="button" type="button"
onClick={() => formActions.setShowConfirmPassword(!uiState.showConfirmPassword)} onClick={() => formActions.setShowConfirmPassword(!uiState.showConfirmPassword)}
style={{ position: 'absolute', right: '0.75rem', top: '50%', transform: 'translateY(-50%)', zIndex: 10, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }} position="absolute"
right="3"
top="50%"
zIndex={10}
bg="transparent"
borderStyle="none"
cursor="pointer"
> >
<Icon icon={uiState.showConfirmPassword ? EyeOff : Eye} size={4} color="#6b7280" /> <Icon icon={uiState.showConfirmPassword ? EyeOff : Eye} size={4} color="#6b7280" />
</Box> </Box>
@@ -408,22 +419,22 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
{mutationState.isPending ? 'Creating account...' : 'Create Account'} {mutationState.isPending ? 'Creating account...' : 'Create Account'}
</Button> </Button>
</Stack> </Stack>
</form> </Box>
{/* Divider */} {/* Divider */}
<Box style={{ position: 'relative' }} my={6}> <Box position="relative" my={6}>
<Box style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center' }}> <Box position="absolute" inset="0" display="flex" alignItems="center">
<Box style={{ width: '100%', borderTop: '1px solid #262626' }} /> <Box w="full" borderTop borderColor="border-neutral-800" />
</Box> </Box>
<Box style={{ position: 'relative', display: 'flex', justifyContent: 'center' }}> <Box position="relative" display="flex" justifyContent="center">
<Box px={4} style={{ backgroundColor: '#171717' }}> <Box px={4} bg="bg-neutral-900">
<Text size="xs" color="text-gray-500">or continue with</Text> <Text size="xs" color="text-gray-500">or continue with</Text>
</Box> </Box>
</Box> </Box>
</Box> </Box>
{/* Login Link */} {/* Login Link */}
<Box style={{ textAlign: 'center' }} mt={6}> <Box textAlign="center" mt={6}>
<Text size="sm" color="text-gray-400"> <Text size="sm" color="text-gray-400">
Already have an account?{' '} Already have an account?{' '}
<Link <Link
@@ -436,7 +447,7 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
</Card> </Card>
{/* Footer */} {/* Footer */}
<Box mt={6} style={{ textAlign: 'center' }}> <Box mt={6} textAlign="center">
<Text size="xs" color="text-gray-500"> <Text size="xs" color="text-gray-500">
By creating an account, you agree to our{' '} By creating an account, you agree to our{' '}
<Link href="/terms"> <Link href="/terms">
@@ -450,12 +461,12 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
</Box> </Box>
{/* Mobile Role Info */} {/* Mobile Role Info */}
<Box mt={8} className="lg:hidden"> <Box mt={8} display={{ base: 'block', lg: 'none' }}>
<Text size="xs" color="text-gray-500" block mb={4} style={{ textAlign: 'center' }}>One account for all roles</Text> <Text size="xs" color="text-gray-500" block mb={4} textAlign="center">One account for all roles</Text>
<Stack direction="row" align="center" justify="center" gap={6}> <Stack direction="row" align="center" justify="center" gap={6}>
{USER_ROLES.map((role) => ( {USER_ROLES.map((role) => (
<Stack key={role.title} align="center" gap={1}> <Stack key={role.title} align="center" gap={1}>
<Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: `${role.color}1A` }}> <Surface variant="muted" rounded="lg" padding={2} bg={role.bg}>
<Icon icon={role.icon} size={4} color={role.color} /> <Icon icon={role.icon} size={4} color={role.color} />
</Surface> </Surface>
<Text size="xs" color="text-gray-500">{role.title}</Text> <Text size="xs" color="text-gray-500">{role.title}</Text>

View File

@@ -11,8 +11,8 @@ interface Race {
scheduledAt: string; scheduledAt: string;
status: 'scheduled' | 'running' | 'completed' | 'cancelled'; status: 'scheduled' | 'running' | 'completed' | 'cancelled';
sessionType: string; sessionType: string;
leagueId?: string; leagueId?: string | null;
leagueName?: string; leagueName?: string | null;
strengthOfField?: number | null; strengthOfField?: number | null;
} }