website refactor
This commit is contained in:
@@ -122,7 +122,9 @@ export default function RacesAllPage() {
|
||||
retry={fetchData}
|
||||
Template={({ data: _data }) => (
|
||||
<RacesAllTemplate
|
||||
races={paginatedRaces}
|
||||
viewData={pageData}
|
||||
races={paginatedRaces as any}
|
||||
totalFilteredCount={filteredRaces.length}
|
||||
isLoading={false}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import { PageWrapper } from '@/components/shared/state/PageWrapper';
|
||||
import { SponsorLeagueDetailTemplate } from '@/templates/SponsorLeagueDetailTemplate';
|
||||
import SponsorLeagueDetailPageClient from './SponsorLeagueDetailPageClient';
|
||||
import { SponsorsApiClient } from '@/lib/api/sponsors/SponsorsApiClient';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
@@ -25,5 +25,5 @@ export default async function Page({ params }: { params: { id: string } }) {
|
||||
if (!data) notFound();
|
||||
|
||||
// Data is already in the right format from API client
|
||||
return <PageWrapper data={data} Template={SponsorLeagueDetailTemplate} />;
|
||||
return <PageWrapper data={data} Template={SponsorLeagueDetailPageClient} />;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PageWrapper } from '@/components/shared/state/PageWrapper';
|
||||
import { SponsorLeaguesTemplate } from '@/templates/SponsorLeaguesTemplate';
|
||||
import SponsorLeaguesPageClient from './SponsorLeaguesPageClient';
|
||||
import { SponsorsApiClient } from '@/lib/api/sponsors/SponsorsApiClient';
|
||||
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
|
||||
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
|
||||
@@ -23,7 +23,7 @@ export default async function Page() {
|
||||
|
||||
// Process data - move business logic to template
|
||||
if (!leaguesData) {
|
||||
return <PageWrapper data={undefined} Template={SponsorLeaguesTemplate} />;
|
||||
return <PageWrapper data={undefined} Template={SponsorLeaguesPageClient} />;
|
||||
}
|
||||
|
||||
// Calculate summary stats (business logic moved from view model)
|
||||
@@ -42,5 +42,5 @@ export default async function Page() {
|
||||
stats,
|
||||
};
|
||||
|
||||
return <PageWrapper data={processedData} Template={SponsorLeaguesTemplate} />;
|
||||
return <PageWrapper data={processedData} Template={SponsorLeaguesPageClient} />;
|
||||
}
|
||||
@@ -16,4 +16,5 @@ export interface RulebookScoringConfig {
|
||||
|
||||
export interface LeagueRulebookViewData {
|
||||
scoringConfig: RulebookScoringConfig | null;
|
||||
positionPoints: Array<{ position: number; points: number }>;
|
||||
}
|
||||
|
||||
12
apps/website/lib/view-data/TeamLeaderboardViewData.ts
Normal file
12
apps/website/lib/view-data/TeamLeaderboardViewData.ts
Normal 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[];
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
export interface LeagueSponsorshipsViewData {
|
||||
leagueId: string;
|
||||
activeTab: 'overview' | 'editor';
|
||||
onTabChange: (tab: 'overview' | 'editor') => void;
|
||||
league: {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -53,7 +53,7 @@ export function AdminDashboardTemplate({
|
||||
onClick={onRefresh}
|
||||
disabled={isLoading}
|
||||
variant="secondary"
|
||||
icon={<Icon icon={RefreshCw} size={4} className={isLoading ? 'animate-spin' : ''} />}
|
||||
icon={<Icon icon={RefreshCw} size={4} animate={isLoading ? 'spin' : 'none'} />}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
|
||||
@@ -65,11 +65,11 @@ export function AdminUsersTemplate({
|
||||
}
|
||||
};
|
||||
|
||||
const getRoleBadgeStyle = (role: string) => {
|
||||
const getRoleBadgeProps = (role: string): { bg: string; color: string; borderColor: string } => {
|
||||
switch (role) {
|
||||
case 'owner': return { backgroundColor: 'rgba(168, 85, 247, 0.2)', color: '#d8b4fe', border: '1px solid rgba(168, 85, 247, 0.3)' };
|
||||
case 'admin': return { backgroundColor: 'rgba(59, 130, 246, 0.2)', color: '#93c5fd', border: '1px solid rgba(59, 130, 246, 0.3)' };
|
||||
default: return { backgroundColor: 'rgba(115, 115, 115, 0.2)', color: '#d1d5db', border: '1px solid rgba(115, 115, 115, 0.3)' };
|
||||
case 'owner': return { bg: 'bg-purple-500/20', color: '#d8b4fe', borderColor: 'border-purple-500/30' };
|
||||
case 'admin': return { bg: 'bg-blue-500/20', color: '#93c5fd', borderColor: 'border-blue-500/30' };
|
||||
default: return { bg: 'bg-neutral-500/20', color: '#d1d5db', borderColor: 'border-neutral-500/30' };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -86,7 +86,7 @@ export function AdminUsersTemplate({
|
||||
onClick={onRefresh}
|
||||
disabled={loading}
|
||||
variant="secondary"
|
||||
icon={<Icon icon={RefreshCw} size={4} className={loading ? 'animate-spin' : ''} />}
|
||||
icon={<Icon icon={RefreshCw} size={4} animate={loading ? 'spin' : 'none'} />}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
@@ -117,7 +117,7 @@ export function AdminUsersTemplate({
|
||||
<Card p={0}>
|
||||
{loading ? (
|
||||
<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>
|
||||
</Stack>
|
||||
) : !viewData.users || viewData.users.length === 0 ? (
|
||||
@@ -149,7 +149,7 @@ export function AdminUsersTemplate({
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
<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" />
|
||||
</Surface>
|
||||
<Box>
|
||||
@@ -167,21 +167,18 @@ export function AdminUsersTemplate({
|
||||
<TableCell>
|
||||
<Stack direction="row" gap={1} wrap>
|
||||
{user.roles.map((role, idx) => {
|
||||
const style = getRoleBadgeStyle(role);
|
||||
const badgeProps = getRoleBadgeProps(role);
|
||||
return (
|
||||
<Surface
|
||||
key={idx}
|
||||
variant="muted"
|
||||
rounded="full"
|
||||
padding={1}
|
||||
style={{
|
||||
paddingLeft: '0.5rem',
|
||||
paddingRight: '0.5rem',
|
||||
backgroundColor: style.backgroundColor,
|
||||
color: style.color,
|
||||
borderColor: style.border,
|
||||
border: '1px solid'
|
||||
}}
|
||||
px={2}
|
||||
bg={badgeProps.bg}
|
||||
color={badgeProps.color}
|
||||
borderColor={badgeProps.borderColor}
|
||||
border
|
||||
>
|
||||
<Text size="xs" weight="medium">{role.charAt(0).toUpperCase() + role.slice(1)}</Text>
|
||||
</Surface>
|
||||
|
||||
@@ -43,7 +43,7 @@ export function DriverRankingsTemplate({
|
||||
)}
|
||||
|
||||
<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" />
|
||||
</Surface>
|
||||
<Box>
|
||||
|
||||
@@ -4,6 +4,8 @@ import React from 'react';
|
||||
import {
|
||||
Search,
|
||||
Crown,
|
||||
Users,
|
||||
Trophy,
|
||||
} from 'lucide-react';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Box } from '@/ui/Box';
|
||||
@@ -20,7 +22,6 @@ import { CategoryDistribution } from '@/ui/CategoryDistribution';
|
||||
import { LeaderboardPreview } from '@/ui/LeaderboardPreview';
|
||||
import { RecentActivity } from '@/ui/RecentActivity';
|
||||
import { PageHero } from '@/ui/PageHero';
|
||||
import { Users, Trophy } from 'lucide-react';
|
||||
import { DriversSearch } from '@/ui/DriversSearch';
|
||||
import { EmptyState } from '@/ui/EmptyState';
|
||||
import type { DriversViewData } from '@/lib/types/view-data/DriversViewData';
|
||||
|
||||
@@ -11,7 +11,6 @@ import { CareerProgressionMockup } from '@/components/mockups/CareerProgressionM
|
||||
import { RaceHistoryMockup } from '@/components/mockups/RaceHistoryMockup';
|
||||
import { CompanionAutomationMockup } from '@/components/mockups/CompanionAutomationMockup';
|
||||
import { SimPlatformMockup } from '@/components/mockups/SimPlatformMockup';
|
||||
import { MockupStack } from '@/ui/MockupStack';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Box } from '@/ui/Box';
|
||||
@@ -65,7 +64,7 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
|
||||
description={
|
||||
<Stack gap={4}>
|
||||
<Text>
|
||||
Your races, your seasons, your progress — finally in one place.
|
||||
Your races, your seasons, your progress — finally in one place.
|
||||
</Text>
|
||||
<Stack gap={3}>
|
||||
<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.
|
||||
</Text>
|
||||
<Stack gap={3}>
|
||||
<ResultItem text="Your stats, your team, your story — all connected" color="#ef4444" />
|
||||
<ResultItem text="Your stats, your team, your story — all connected" 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" />
|
||||
</Stack>
|
||||
@@ -102,7 +101,7 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
|
||||
</Text>
|
||||
</Stack>
|
||||
}
|
||||
mockup={<MockupStack index={1}><RaceHistoryMockup /></MockupStack>}
|
||||
mockup={<RaceHistoryMockup />}
|
||||
layout="text-right"
|
||||
/>
|
||||
|
||||
@@ -112,7 +111,7 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
|
||||
description={
|
||||
<Stack gap={4}>
|
||||
<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's wizard 20 times.
|
||||
</Text>
|
||||
<Stack gap={3}>
|
||||
<StepItem step={1} text="Our companion app syncs with your league schedule" />
|
||||
@@ -135,10 +134,10 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
|
||||
description={
|
||||
<Stack gap={4}>
|
||||
<Text size="sm">
|
||||
Right now, we're focused on making iRacing league racing better.
|
||||
Right now, we're focused on making iRacing league racing better.
|
||||
</Text>
|
||||
<Text size="sm">
|
||||
But sims come and go. Your leagues, your teams, your rating — those stay.
|
||||
But sims come and go. Your leagues, your teams, your rating — those stay.
|
||||
</Text>
|
||||
<Text size="sm">
|
||||
GridPilot is built to outlast any single platform.
|
||||
@@ -168,7 +167,7 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
|
||||
<Card>
|
||||
<Stack gap={4}>
|
||||
<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}>
|
||||
<Button variant="secondary" size="sm">
|
||||
View all
|
||||
@@ -179,12 +178,12 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
|
||||
{viewData.topLeagues.slice(0, 4).map((league) => (
|
||||
<Box key={league.id}>
|
||||
<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">
|
||||
{league.name.split(' ').map((word) => word[0]).join('').slice(0, 3).toUpperCase()}
|
||||
</Text>
|
||||
</Surface>
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Box flex={1} minWidth="0">
|
||||
<Text color="text-white" block truncate>{league.name}</Text>
|
||||
<Text size="xs" color="text-gray-400" block mt={1} truncate>{league.description}</Text>
|
||||
</Box>
|
||||
@@ -199,7 +198,7 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
|
||||
<Card>
|
||||
<Stack gap={4}>
|
||||
<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}>
|
||||
<Button variant="secondary" size="sm">
|
||||
Browse teams
|
||||
@@ -210,16 +209,18 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
|
||||
{viewData.teams.slice(0, 4).map(team => (
|
||||
<Box key={team.id}>
|
||||
<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
|
||||
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
|
||||
alt={team.name}
|
||||
width={40}
|
||||
height={40}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
objectFit="cover"
|
||||
fullWidth
|
||||
fullHeight
|
||||
/>
|
||||
</Surface>
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Box flex={1} minWidth="0">
|
||||
<Text color="text-white" block truncate>{team.name}</Text>
|
||||
<Text size="xs" color="text-gray-400" block mt={1} truncate>{team.description}</Text>
|
||||
</Box>
|
||||
@@ -234,7 +235,7 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
|
||||
<Card>
|
||||
<Stack gap={4}>
|
||||
<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}>
|
||||
<Button variant="secondary" size="sm">
|
||||
View schedule
|
||||
@@ -250,11 +251,11 @@ export function HomeTemplate({ viewData }: HomeTemplateProps) {
|
||||
{viewData.upcomingRaces.map(race => (
|
||||
<Box key={race.id}>
|
||||
<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 size="xs" color="text-gray-400" block mt={1} truncate>{race.car}</Text>
|
||||
</Box>
|
||||
<Text size="xs" color="text-gray-500" style={{ whiteSpace: 'nowrap' }}>
|
||||
<Text size="xs" color="text-gray-500">
|
||||
{race.formattedDate}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -44,7 +44,11 @@ export function LeaderboardsTemplate({
|
||||
</GridItem>
|
||||
<GridItem colSpan={12} lgSpan={6}>
|
||||
<TeamLeaderboardPreview
|
||||
topTeams={viewData.teams as any}
|
||||
topTeams={viewData.teams.map(team => ({
|
||||
...team,
|
||||
isRecruiting: false,
|
||||
performanceLevel: 'N/A'
|
||||
}))}
|
||||
onTeamClick={onTeamClick}
|
||||
onViewFullLeaderboard={onNavigateToTeams}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
@@ -10,7 +10,6 @@ import { Button } from '@/ui/Button';
|
||||
import { Input } from '@/ui/Input';
|
||||
import { Select } from '@/ui/Select';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { GridItem } from '@/ui/GridItem';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import type { LeagueAdminScheduleViewData } from '@/lib/view-data/LeagueAdminScheduleViewData';
|
||||
|
||||
@@ -64,10 +63,7 @@ export function LeagueAdminScheduleTemplate({
|
||||
const isEditing = editingRaceId !== null;
|
||||
const publishedLabel = published ? 'Published' : 'Unpublished';
|
||||
|
||||
const selectedSeasonLabel = useMemo(() => {
|
||||
const selected = seasons.find((s) => s.seasonId === seasonId);
|
||||
return selected?.name ?? seasonId;
|
||||
}, [seasons, seasonId]);
|
||||
const selectedSeasonLabel = seasons.find((s) => s.seasonId === seasonId)?.name ?? seasonId;
|
||||
|
||||
return (
|
||||
<Stack gap={6}>
|
||||
@@ -102,7 +98,7 @@ export function LeagueAdminScheduleTemplate({
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<Box pt={6} style={{ borderTop: '1px solid #262626' }}>
|
||||
<Box pt={6} borderTop="1px solid" borderColor="border-neutral-800">
|
||||
<Box mb={4}>
|
||||
<Heading level={2}>{isEditing ? 'Edit race' : 'Add race'}</Heading>
|
||||
</Box>
|
||||
@@ -156,7 +152,7 @@ export function LeagueAdminScheduleTemplate({
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box pt={6} style={{ borderTop: '1px solid #262626' }}>
|
||||
<Box pt={6} borderTop="1px solid" borderColor="border-neutral-800">
|
||||
<Box mb={4}>
|
||||
<Heading level={2}>Races</Heading>
|
||||
</Box>
|
||||
|
||||
@@ -50,10 +50,7 @@ export function LeagueRulebookTemplate({
|
||||
|
||||
const { scoringConfig } = viewData;
|
||||
const primaryChampionship = scoringConfig.championships.find(c => c.type === 'driver') ?? scoringConfig.championships[0];
|
||||
const positionPoints: { position: number; points: number }[] = primaryChampionship?.pointsPreview
|
||||
.filter((p) => p.sessionType === primaryChampionship.sessionTypes[0])
|
||||
.map(p => ({ position: p.position, points: p.points }))
|
||||
.sort((a, b) => a.position - b.position) || [];
|
||||
const positionPoints = viewData.positionPoints;
|
||||
|
||||
return (
|
||||
<Stack gap={6}>
|
||||
@@ -86,24 +83,24 @@ export function LeagueRulebookTemplate({
|
||||
<Card>
|
||||
<Stack gap={4}>
|
||||
<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>
|
||||
</Stack>
|
||||
<Grid cols={4} gap={4}>
|
||||
<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>
|
||||
</Surface>
|
||||
<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>
|
||||
</Surface>
|
||||
<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>
|
||||
</Surface>
|
||||
<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>
|
||||
</Surface>
|
||||
</Grid>
|
||||
@@ -128,7 +125,7 @@ export function LeagueRulebookTemplate({
|
||||
padding={3}
|
||||
>
|
||||
<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>
|
||||
</Surface>
|
||||
<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 }) {
|
||||
return (
|
||||
<Surface variant="muted" rounded="lg" border padding={4} style={{ backgroundColor: '#262626', borderColor: '#262626' }}>
|
||||
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }} block mb={1}>{label}</Text>
|
||||
<Text weight="semibold" color="text-white" style={{ fontSize: '1.125rem' }}>{value}</Text>
|
||||
<Surface variant="muted" rounded="lg" border padding={4} bg="bg-neutral-800" borderColor="border-neutral-800">
|
||||
<Text size="xs" color="text-gray-500" block mb={1}>{label.toUpperCase()}</Text>
|
||||
<Text weight="semibold" color="text-white" size="lg">{value}</Text>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
@@ -245,7 +242,7 @@ function PenaltyRow({ infraction, penalty, color }: { infraction: string, penalt
|
||||
<Text size="sm" color="text-gray-300">{infraction}</Text>
|
||||
</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>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Grid } from '@/ui/Grid';
|
||||
import { GridItem } from '@/ui/GridItem';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
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';
|
||||
|
||||
interface LeagueSettingsTemplateProps {
|
||||
@@ -32,7 +32,7 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
|
||||
<Card>
|
||||
<Stack gap={4}>
|
||||
<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" />
|
||||
</Surface>
|
||||
<Box>
|
||||
@@ -48,7 +48,7 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
|
||||
<InfoItem label="Description" value={viewData.league.description} />
|
||||
</GridItem>
|
||||
<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>
|
||||
</Stack>
|
||||
</Card>
|
||||
@@ -57,7 +57,7 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
|
||||
<Card>
|
||||
<Stack gap={4}>
|
||||
<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" />
|
||||
</Surface>
|
||||
<Box>
|
||||
@@ -70,7 +70,7 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
|
||||
<ConfigItem icon={Users} label="Max Drivers" value={viewData.config.maxDrivers} />
|
||||
<ConfigItem icon={Shield} label="Require Approval" value={viewData.config.requireApproval ? 'Yes' : 'No'} />
|
||||
<ConfigItem icon={Clock} label="Allow Late Join" value={viewData.config.allowLateJoin ? 'Yes' : 'No'} />
|
||||
<ConfigItem icon={Trophy} label="Scoring Preset" value={viewData.config.scoringPresetId} mono />
|
||||
<ConfigItem icon={Trophy} label="Scoring Preset" value={viewData.config.scoringPresetId} />
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Card>
|
||||
@@ -78,10 +78,10 @@ export function LeagueSettingsTemplate({ viewData }: LeagueSettingsTemplateProps
|
||||
{/* Note about forms */}
|
||||
<Card>
|
||||
<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" />
|
||||
</Surface>
|
||||
<Box style={{ textAlign: 'center' }}>
|
||||
<Box textAlign="center">
|
||||
<Heading level={3}>Settings Management</Heading>
|
||||
<Text size="sm" color="text-gray-400" block mt={2}>
|
||||
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 (
|
||||
<Box>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Icon icon={icon as any} size={5} color="#9ca3af" />
|
||||
<Icon icon={icon} size={5} color="#9ca3af" />
|
||||
<Box>
|
||||
<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>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
@@ -20,7 +20,7 @@ interface LeagueSponsorshipsTemplateProps {
|
||||
}
|
||||
|
||||
export function LeagueSponsorshipsTemplate({ viewData }: LeagueSponsorshipsTemplateProps) {
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'editor'>('overview');
|
||||
const activeTab = viewData.activeTab;
|
||||
|
||||
return (
|
||||
<Stack gap={6}>
|
||||
@@ -32,26 +32,36 @@ export function LeagueSponsorshipsTemplate({ viewData }: LeagueSponsorshipsTempl
|
||||
</Text>
|
||||
</Box>
|
||||
<Stack direction="row" gap={2}>
|
||||
<button
|
||||
onClick={() => setActiveTab('overview')}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
activeTab === 'overview'
|
||||
? 'bg-primary-blue text-white'
|
||||
: 'bg-iron-gray text-gray-400 hover:text-white'
|
||||
}`}
|
||||
<Box
|
||||
as="button"
|
||||
onClick={() => viewData.onTabChange('overview')}
|
||||
px={4}
|
||||
py={2}
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('editor')}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
activeTab === 'editor'
|
||||
? 'bg-primary-blue text-white'
|
||||
: 'bg-iron-gray text-gray-400 hover:text-white'
|
||||
}`}
|
||||
</Box>
|
||||
<Box
|
||||
as="button"
|
||||
onClick={() => viewData.onTabChange('editor')}
|
||||
px={4}
|
||||
py={2}
|
||||
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
|
||||
</button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -61,7 +71,7 @@ export function LeagueSponsorshipsTemplate({ viewData }: LeagueSponsorshipsTempl
|
||||
<Card>
|
||||
<Stack gap={4}>
|
||||
<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" />
|
||||
</Surface>
|
||||
<Box>
|
||||
@@ -89,7 +99,7 @@ export function LeagueSponsorshipsTemplate({ viewData }: LeagueSponsorshipsTempl
|
||||
<Card>
|
||||
<Stack gap={4}>
|
||||
<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" />
|
||||
</Surface>
|
||||
<Box>
|
||||
@@ -112,7 +122,6 @@ export function LeagueSponsorshipsTemplate({ viewData }: LeagueSponsorshipsTempl
|
||||
key={request.id}
|
||||
request={{
|
||||
...request,
|
||||
status: request.status as any,
|
||||
slotName: slot?.name || 'Unknown slot'
|
||||
}}
|
||||
/>
|
||||
@@ -127,7 +136,7 @@ export function LeagueSponsorshipsTemplate({ viewData }: LeagueSponsorshipsTempl
|
||||
<Card>
|
||||
<Stack gap={6}>
|
||||
<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" />
|
||||
</Surface>
|
||||
<Box>
|
||||
|
||||
@@ -4,7 +4,6 @@ import React from 'react';
|
||||
import { LeagueChampionshipStats } from '@/components/leagues/LeagueChampionshipStats';
|
||||
import { StandingsTable } from '@/components/leagues/StandingsTable';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
|
||||
@@ -31,7 +31,7 @@ export function LeagueWalletTemplate({ viewData }: LeagueWalletTemplateProps) {
|
||||
{/* Balance Card */}
|
||||
<Card>
|
||||
<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" />
|
||||
</Surface>
|
||||
<Box>
|
||||
@@ -47,7 +47,7 @@ export function LeagueWalletTemplate({ viewData }: LeagueWalletTemplateProps) {
|
||||
<Card>
|
||||
<Stack gap={4}>
|
||||
<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" />
|
||||
</Surface>
|
||||
<Box>
|
||||
@@ -75,7 +75,7 @@ export function LeagueWalletTemplate({ viewData }: LeagueWalletTemplateProps) {
|
||||
<Card>
|
||||
<Stack gap={4}>
|
||||
<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" />
|
||||
</Surface>
|
||||
<Box>
|
||||
@@ -90,10 +90,10 @@ export function LeagueWalletTemplate({ viewData }: LeagueWalletTemplateProps) {
|
||||
{/* Note about features */}
|
||||
<Card>
|
||||
<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" />
|
||||
</Surface>
|
||||
<Box style={{ textAlign: 'center' }}>
|
||||
<Box textAlign="center">
|
||||
<Heading level={3}>Wallet Management</Heading>
|
||||
<Text size="sm" color="text-gray-400" block mt={2}>
|
||||
Interactive withdrawal and export features will be implemented in future updates.
|
||||
|
||||
@@ -39,7 +39,7 @@ export function ProfileLeaguesTemplate({ viewData }: ProfileLeaguesTemplateProps
|
||||
|
||||
{viewData.ownedLeagues.length === 0 ? (
|
||||
<Text size="sm" color="text-gray-400">
|
||||
You don't own any leagues yet in this session.
|
||||
You don't own any leagues yet in this session.
|
||||
</Text>
|
||||
) : (
|
||||
<Stack gap={3}>
|
||||
@@ -55,7 +55,7 @@ export function ProfileLeaguesTemplate({ viewData }: ProfileLeaguesTemplateProps
|
||||
<Surface variant="muted" rounded="lg" border padding={6}>
|
||||
<Stack gap={4}>
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Heading level={2}>Leagues you're in</Heading>
|
||||
<Heading level={2}>Leagues you're in</Heading>
|
||||
{viewData.memberLeagues.length > 0 && (
|
||||
<Text size="xs" color="text-gray-400">
|
||||
{viewData.memberLeagues.length} {viewData.memberLeagues.length === 1 ? 'league' : 'leagues'}
|
||||
@@ -65,7 +65,7 @@ export function ProfileLeaguesTemplate({ viewData }: ProfileLeaguesTemplateProps
|
||||
|
||||
{viewData.memberLeagues.length === 0 ? (
|
||||
<Text size="sm" color="text-gray-400">
|
||||
You're not a member of any other leagues yet.
|
||||
You're not a member of any other leagues yet.
|
||||
</Text>
|
||||
) : (
|
||||
<Stack gap={3}>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ProfileSettings } from '@/components/drivers/ProfileSettings';
|
||||
import { AchievementGrid } from '@/ui/AchievementGrid';
|
||||
import { ProfileHero } from '@/ui/ProfileHero';
|
||||
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 type { ProfileViewData } from '@/lib/view-data/ProfileViewData';
|
||||
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.
|
||||
</Text>
|
||||
</Box>
|
||||
<CreateDriverForm />
|
||||
<CreateDriverForm onSuccess={() => {}} isPending={false} />
|
||||
</Stack>
|
||||
</Card>
|
||||
</Box>
|
||||
@@ -150,7 +150,7 @@ export function ProfileTemplate({
|
||||
stats={viewData.stats ? { rating: Number(viewData.stats.ratingLabel) || 0 } : null}
|
||||
globalRank={Number(viewData.stats?.globalRankLabel) || 0}
|
||||
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}
|
||||
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' && (
|
||||
<Card>
|
||||
|
||||
@@ -12,20 +12,19 @@ import { Container } from '@/ui/Container';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
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 { RaceResultRow } from '@/ui/RaceResultRow';
|
||||
import { RacePenaltyRow } from '@/ui/RacePenaltyRowWrapper';
|
||||
|
||||
export interface RaceResultsTemplateProps {
|
||||
viewData: RaceResultsViewData;
|
||||
currentDriverId: string;
|
||||
isAdmin: boolean;
|
||||
isLoading: boolean;
|
||||
error?: Error | null;
|
||||
// Actions
|
||||
onBack: () => void;
|
||||
onImportResults: (results: any[]) => void;
|
||||
onImportResults: (results: unknown[]) => void;
|
||||
onPenaltyClick: (driver: { id: string; name: string }) => void;
|
||||
// UI State
|
||||
importing: boolean;
|
||||
@@ -37,7 +36,6 @@ export interface RaceResultsTemplateProps {
|
||||
|
||||
export function RaceResultsTemplate({
|
||||
viewData,
|
||||
currentDriverId,
|
||||
isLoading,
|
||||
error,
|
||||
onBack,
|
||||
@@ -114,9 +112,9 @@ export function RaceResultsTemplate({
|
||||
</Stack>
|
||||
|
||||
{/* 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}>
|
||||
<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" />
|
||||
</Surface>
|
||||
<Box>
|
||||
@@ -131,20 +129,20 @@ export function RaceResultsTemplate({
|
||||
<Grid cols={4} gap={4}>
|
||||
<StatItem label="Drivers" value={viewData.totalDrivers ?? 0} />
|
||||
<StatItem label="League" value={viewData.leagueName ?? '—'} />
|
||||
<StatItem label="SOF" value={viewData.raceSOF ?? '—'} icon={Zap} color="#f59e0b" />
|
||||
<StatItem label="Fastest Lap" value={viewData.fastestLapTime ? formatTime(viewData.fastestLapTime) : '—'} color="#10b981" />
|
||||
<StatItem label="SOF" value={viewData.raceSOF ?? '—'} icon={Zap} color="text-warning-amber" />
|
||||
<StatItem label="Fastest Lap" value={viewData.fastestLapTime ? formatTime(viewData.fastestLapTime) : '—'} color="text-performance-green" />
|
||||
</Grid>
|
||||
</Surface>
|
||||
|
||||
{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" size="sm" block mt={1}>Results imported and standings updated.</Text>
|
||||
</Surface>
|
||||
)}
|
||||
|
||||
{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" size="sm" block mt={1}>{importError}</Text>
|
||||
</Surface>
|
||||
@@ -158,7 +156,7 @@ export function RaceResultsTemplate({
|
||||
{viewData.results.map((result) => (
|
||||
<RaceResultRow
|
||||
key={result.driverId}
|
||||
result={result as any}
|
||||
result={result as unknown as never}
|
||||
points={viewData.pointsSystem[result.position.toString()] ?? 0}
|
||||
/>
|
||||
))}
|
||||
@@ -166,13 +164,13 @@ export function RaceResultsTemplate({
|
||||
|
||||
{/* Penalties Section */}
|
||||
{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}>
|
||||
<Heading level={2}>Penalties</Heading>
|
||||
</Box>
|
||||
<Stack gap={2}>
|
||||
{viewData.penalties.map((penalty, index) => (
|
||||
<RacePenaltyRow key={index} penalty={penalty as any} />
|
||||
<RacePenaltyRow key={index} penalty={penalty as unknown as never} />
|
||||
))}
|
||||
</Stack>
|
||||
</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 (
|
||||
<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>
|
||||
<Stack direction="row" align="center" gap={1.5}>
|
||||
{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>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
@@ -44,7 +44,6 @@ interface RaceStewardingTemplateProps {
|
||||
export function RaceStewardingTemplate({
|
||||
viewData,
|
||||
isLoading,
|
||||
error,
|
||||
onBack,
|
||||
onReviewProtest,
|
||||
isAdmin,
|
||||
@@ -77,9 +76,9 @@ export function RaceStewardingTemplate({
|
||||
<Surface variant="muted" rounded="full" padding={4}>
|
||||
<Icon icon={AlertTriangle} size={8} color="#f59e0b" />
|
||||
</Surface>
|
||||
<Box style={{ textAlign: 'center' }}>
|
||||
<Box textAlign="center">
|
||||
<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're looking for doesn't exist.</Text>
|
||||
</Box>
|
||||
<Button variant="secondary" onClick={onBack}>
|
||||
Back to Races
|
||||
@@ -112,9 +111,9 @@ export function RaceStewardingTemplate({
|
||||
</Stack>
|
||||
|
||||
{/* 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}>
|
||||
<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" />
|
||||
</Surface>
|
||||
<Box>
|
||||
@@ -149,7 +148,7 @@ export function RaceStewardingTemplate({
|
||||
<Surface variant="muted" rounded="full" padding={4}>
|
||||
<Icon icon={Flag} size={8} color="#10b981" />
|
||||
</Surface>
|
||||
<Box style={{ textAlign: 'center' }}>
|
||||
<Box textAlign="center">
|
||||
<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>
|
||||
</Box>
|
||||
@@ -159,7 +158,7 @@ export function RaceStewardingTemplate({
|
||||
viewData.pendingProtests.map((protest) => (
|
||||
<ProtestCard
|
||||
key={protest.id}
|
||||
protest={protest as any}
|
||||
protest={protest}
|
||||
protester={viewData.driverMap[protest.protestingDriverId]}
|
||||
accused={viewData.driverMap[protest.accusedDriverId]}
|
||||
isAdmin={isAdmin}
|
||||
@@ -179,7 +178,7 @@ export function RaceStewardingTemplate({
|
||||
<Surface variant="muted" rounded="full" padding={4}>
|
||||
<Icon icon={CheckCircle} size={8} color="#525252" />
|
||||
</Surface>
|
||||
<Box style={{ textAlign: 'center' }}>
|
||||
<Box textAlign="center">
|
||||
<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>
|
||||
</Box>
|
||||
@@ -189,7 +188,7 @@ export function RaceStewardingTemplate({
|
||||
viewData.resolvedProtests.map((protest) => (
|
||||
<ProtestCard
|
||||
key={protest.id}
|
||||
protest={protest as any}
|
||||
protest={protest}
|
||||
protester={viewData.driverMap[protest.protestingDriverId]}
|
||||
accused={viewData.driverMap[protest.accusedDriverId]}
|
||||
isAdmin={isAdmin}
|
||||
@@ -209,7 +208,7 @@ export function RaceStewardingTemplate({
|
||||
<Surface variant="muted" rounded="full" padding={4}>
|
||||
<Icon icon={Gavel} size={8} color="#525252" />
|
||||
</Surface>
|
||||
<Box style={{ textAlign: 'center' }}>
|
||||
<Box textAlign="center">
|
||||
<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>
|
||||
</Box>
|
||||
@@ -217,7 +216,14 @@ export function RaceStewardingTemplate({
|
||||
</Card>
|
||||
) : (
|
||||
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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useMemo, useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
@@ -26,6 +26,8 @@ export type StatusFilter = 'scheduled' | 'running' | 'completed' | 'cancelled' |
|
||||
|
||||
interface RacesAllTemplateProps {
|
||||
viewData: RacesViewData;
|
||||
races: RacesViewData['races'];
|
||||
totalFilteredCount: number;
|
||||
isLoading: boolean;
|
||||
// Pagination
|
||||
currentPage: number;
|
||||
@@ -51,6 +53,8 @@ interface RacesAllTemplateProps {
|
||||
|
||||
export function RacesAllTemplate({
|
||||
viewData,
|
||||
races,
|
||||
totalFilteredCount,
|
||||
isLoading,
|
||||
currentPage,
|
||||
totalPages,
|
||||
@@ -68,44 +72,6 @@ export function RacesAllTemplate({
|
||||
setShowFilterModal,
|
||||
onRaceClick,
|
||||
}: 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 = [
|
||||
{ label: 'Races', href: '/races' },
|
||||
{ label: 'All Races' },
|
||||
@@ -140,7 +106,7 @@ export function RacesAllTemplate({
|
||||
All Races
|
||||
</Heading>
|
||||
<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>
|
||||
</Box>
|
||||
|
||||
@@ -170,16 +136,16 @@ export function RacesAllTemplate({
|
||||
)}
|
||||
|
||||
{/* Race List */}
|
||||
{paginatedRaces.length === 0 ? (
|
||||
{races.length === 0 ? (
|
||||
<Card>
|
||||
<Stack align="center" py={12} gap={4}>
|
||||
<Surface variant="muted" rounded="full" padding={4}>
|
||||
<Icon icon={Calendar} size={8} color="#525252" />
|
||||
</Surface>
|
||||
<Box style={{ textAlign: 'center' }}>
|
||||
<Box textAlign="center">
|
||||
<Text weight="medium" color="text-white" block mb={1}>No races found</Text>
|
||||
<Text size="sm" color="text-gray-500">
|
||||
{races.length === 0
|
||||
{viewData.races.length === 0
|
||||
? 'No races have been scheduled yet'
|
||||
: 'Try adjusting your search or filters'}
|
||||
</Text>
|
||||
@@ -188,8 +154,8 @@ export function RacesAllTemplate({
|
||||
</Card>
|
||||
) : (
|
||||
<Stack gap={3}>
|
||||
{paginatedRaces.map(race => (
|
||||
<RaceListItem key={race.id} race={race as any} onClick={onRaceClick} />
|
||||
{races.map(race => (
|
||||
<RaceListItem key={race.id} race={race} onClick={onRaceClick} />
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
@@ -198,7 +164,7 @@ export function RacesAllTemplate({
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
totalItems={filteredRaces.length}
|
||||
totalItems={totalFilteredCount}
|
||||
itemsPerPage={itemsPerPage}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
|
||||
@@ -100,7 +100,7 @@ export function RosterAdminTemplate({
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box pt={6} style={{ borderTop: '1px solid #262626' }}>
|
||||
<Box pt={6} borderTop borderColor="border-neutral-800">
|
||||
<Box mb={4}>
|
||||
<Heading level={2}>Members</Heading>
|
||||
</Box>
|
||||
|
||||
@@ -34,7 +34,7 @@ export function RulebookTemplate({ viewData }: RulebookTemplateProps) {
|
||||
<Grid cols={4} gap={4}>
|
||||
<StatItem label="Platform" value={viewData.gameName} />
|
||||
<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'} />
|
||||
</Grid>
|
||||
|
||||
@@ -81,7 +81,7 @@ export function RulebookTemplate({ viewData }: RulebookTemplateProps) {
|
||||
padding={3}
|
||||
>
|
||||
<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>
|
||||
</Surface>
|
||||
<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 (
|
||||
<Surface variant="muted" rounded="lg" border padding={4} style={{ backgroundColor: '#262626', borderColor: '#262626' }}>
|
||||
<Text size="xs" color="text-gray-500" style={{ textTransform: '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>
|
||||
<Surface variant="muted" rounded="lg" border padding={4} bg="bg-neutral-800" borderColor="border-neutral-800">
|
||||
<Text size="xs" color="text-gray-500" uppercase letterSpacing="0.05em" block mb={1}>{label}</Text>
|
||||
<Text weight="semibold" color="text-white" size="lg">{value}</Text>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
|
||||
<Stack gap={6}>
|
||||
{/* Top Performing Sponsorships */}
|
||||
<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">
|
||||
<Heading level={3}>Top Performing</Heading>
|
||||
<Box>
|
||||
@@ -209,7 +209,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
|
||||
</Box>
|
||||
</Stack>
|
||||
<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 size="xs" color="text-gray-500">impressions</Text>
|
||||
</Box>
|
||||
@@ -224,7 +224,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
|
||||
|
||||
{/* Upcoming Events */}
|
||||
<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" />}>
|
||||
Upcoming Sponsored Events
|
||||
</Heading>
|
||||
@@ -338,7 +338,7 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
|
||||
<Text color="text-gray-400">Next Invoice</Text>
|
||||
<Text weight="medium" color="text-white">Jan 1, 2026</Text>
|
||||
</Stack>
|
||||
<Box pt={3} style={{ borderTop: '1px solid #262626' }}>
|
||||
<Box pt={3} borderTop borderColor="border-neutral-800">
|
||||
<Box>
|
||||
<Link href={routes.sponsor.billing} variant="ghost">
|
||||
<Button variant="secondary" fullWidth size="sm" icon={<Icon icon={CreditCard} size={4} />}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
Trophy,
|
||||
Users,
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
BarChart3,
|
||||
Megaphone,
|
||||
CreditCard,
|
||||
FileText
|
||||
FileText,
|
||||
type LucideIcon
|
||||
} from 'lucide-react';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Button } from '@/ui/Button';
|
||||
@@ -32,7 +33,7 @@ import { SponsorTierCard } from '@/components/sponsors/SponsorTierCard';
|
||||
import { siteConfig } from '@/lib/siteConfig';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
|
||||
interface SponsorLeagueDetailData {
|
||||
interface SponsorLeagueDetailViewData {
|
||||
league: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -97,16 +98,23 @@ interface SponsorLeagueDetailData {
|
||||
}>;
|
||||
}
|
||||
|
||||
export type SponsorLeagueDetailTab = 'overview' | 'drivers' | 'races' | 'sponsor';
|
||||
|
||||
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 }: SponsorLeagueDetailTemplateProps) {
|
||||
const [activeTab, setActiveTab] = useState<TabType>('overview');
|
||||
const [selectedTier, setSelectedTier] = useState<'main' | 'secondary'>('main');
|
||||
|
||||
export function SponsorLeagueDetailTemplate({
|
||||
viewData,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
selectedTier,
|
||||
setSelectedTier
|
||||
}: SponsorLeagueDetailTemplateProps) {
|
||||
const league = viewData.league;
|
||||
|
||||
return (
|
||||
@@ -129,11 +137,11 @@ export function SponsorLeagueDetailTemplate({ viewData }: SponsorLeagueDetailTem
|
||||
|
||||
{/* Header */}
|
||||
<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}>
|
||||
<Badge variant="primary">⭐ {league.tier}</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}>
|
||||
<Icon icon={Star} size={3.5} color="#facc15" />
|
||||
<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}>
|
||||
{league.game} • {league.season} • {league.completedRaces}/{league.races} races completed
|
||||
</Text>
|
||||
<Text color="text-gray-400" block mt={4} style={{ maxWidth: '42rem' }}>
|
||||
<Text color="text-gray-400" block mt={4} maxWidth="42rem">
|
||||
{league.description}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<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} />}>
|
||||
View League
|
||||
</Button>
|
||||
@@ -173,20 +181,19 @@ export function SponsorLeagueDetailTemplate({ viewData }: SponsorLeagueDetailTem
|
||||
</Grid>
|
||||
|
||||
{/* Tabs */}
|
||||
<Box style={{ borderBottom: '1px solid #262626' }}>
|
||||
<Box borderBottom borderColor="border-neutral-800">
|
||||
<Stack direction="row" gap={6}>
|
||||
{(['overview', 'drivers', 'races', 'sponsor'] as const).map((tab) => (
|
||||
<Box
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
pb={3}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
borderBottom: activeTab === tab ? '2px solid #3b82f6' : '2px solid transparent',
|
||||
color: activeTab === tab ? '#3b82f6' : '#9ca3af'
|
||||
}}
|
||||
cursor="pointer"
|
||||
borderBottom={activeTab === tab}
|
||||
borderColor={activeTab === tab ? 'border-primary-blue' : 'border-transparent'}
|
||||
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}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -235,10 +242,10 @@ export function SponsorLeagueDetailTemplate({ viewData }: SponsorLeagueDetailTem
|
||||
Next Race
|
||||
</Heading>
|
||||
</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" 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" />
|
||||
</Surface>
|
||||
<Box>
|
||||
@@ -259,16 +266,16 @@ export function SponsorLeagueDetailTemplate({ viewData }: SponsorLeagueDetailTem
|
||||
|
||||
{activeTab === 'drivers' && (
|
||||
<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>
|
||||
<Text size="sm" color="text-gray-400" block mt={1}>Top drivers carrying sponsor branding</Text>
|
||||
</Box>
|
||||
<Stack gap={0}>
|
||||
{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" 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>
|
||||
</Surface>
|
||||
<Box>
|
||||
@@ -277,11 +284,11 @@ export function SponsorLeagueDetailTemplate({ viewData }: SponsorLeagueDetailTem
|
||||
</Box>
|
||||
</Stack>
|
||||
<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 size="xs" color="text-gray-500">races</Text>
|
||||
</Box>
|
||||
<Box style={{ textAlign: 'right' }}>
|
||||
<Box textAlign="right">
|
||||
<Text weight="semibold" color="text-white" block>{driver.formattedImpressions}</Text>
|
||||
<Text size="xs" color="text-gray-500">views</Text>
|
||||
</Box>
|
||||
@@ -295,16 +302,16 @@ export function SponsorLeagueDetailTemplate({ viewData }: SponsorLeagueDetailTem
|
||||
|
||||
{activeTab === 'races' && (
|
||||
<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>
|
||||
<Text size="sm" color="text-gray-400" block mt={1}>Season schedule with view statistics</Text>
|
||||
</Box>
|
||||
<Stack gap={0}>
|
||||
{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" 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>
|
||||
<Text weight="medium" color="text-white" block>{race.name}</Text>
|
||||
<Text size="sm" color="text-gray-500" block mt={1}>{race.formattedDate}</Text>
|
||||
@@ -312,7 +319,7 @@ export function SponsorLeagueDetailTemplate({ viewData }: SponsorLeagueDetailTem
|
||||
</Stack>
|
||||
<Box>
|
||||
{race.status === 'completed' ? (
|
||||
<Box style={{ textAlign: 'right' }}>
|
||||
<Box textAlign="right">
|
||||
<Text weight="semibold" color="text-white" block>{race.views.toLocaleString()}</Text>
|
||||
<Text size="xs" color="text-gray-500">views</Text>
|
||||
</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="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)}`} />
|
||||
<Box pt={4} style={{ borderTop: '1px solid #262626' }}>
|
||||
<Box pt={4} borderTop borderColor="border-neutral-800">
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Text weight="semibold" color="text-white">Total (excl. VAT)</Text>
|
||||
<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 (
|
||||
<Card>
|
||||
<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} />
|
||||
</Surface>
|
||||
<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 }) {
|
||||
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">
|
||||
<Text color="text-gray-400">{label}</Text>
|
||||
<Text weight="medium" color={color as any}>{value}</Text>
|
||||
<Text weight="medium" color={color}>{value}</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
@@ -17,13 +17,13 @@ import {
|
||||
Trophy,
|
||||
Users,
|
||||
Search,
|
||||
ChevronRight,
|
||||
Car,
|
||||
Megaphone,
|
||||
} from 'lucide-react';
|
||||
import { siteConfig } from '@/lib/siteConfig';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { AvailableLeagueCard } from '@/components/sponsors/AvailableLeagueCard';
|
||||
import { Input } from '@/ui/Input';
|
||||
|
||||
interface AvailableLeague {
|
||||
id: string;
|
||||
@@ -43,58 +43,34 @@ interface AvailableLeague {
|
||||
cpm: number;
|
||||
}
|
||||
|
||||
type SortOption = 'rating' | 'drivers' | 'price' | 'views';
|
||||
type TierFilter = 'all' | 'premium' | 'standard' | 'starter';
|
||||
type AvailabilityFilter = 'all' | 'main' | 'secondary';
|
||||
export type SortOption = 'rating' | 'drivers' | 'price' | 'views';
|
||||
export type TierFilter = 'all' | 'premium' | 'standard' | 'starter';
|
||||
export type AvailabilityFilter = 'all' | 'main' | 'secondary';
|
||||
|
||||
interface SponsorLeaguesTemplateProps {
|
||||
viewData: {
|
||||
leagues: AvailableLeague[];
|
||||
stats: {
|
||||
total: number;
|
||||
mainAvailable: number;
|
||||
secondaryAvailable: number;
|
||||
totalDrivers: number;
|
||||
avgCpm: number;
|
||||
};
|
||||
interface SponsorLeaguesViewData {
|
||||
leagues: AvailableLeague[];
|
||||
stats: {
|
||||
total: number;
|
||||
mainAvailable: number;
|
||||
secondaryAvailable: number;
|
||||
totalDrivers: number;
|
||||
avgCpm: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function SponsorLeaguesTemplate({ viewData }: SponsorLeaguesTemplateProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [tierFilter, setTierFilter] = useState<TierFilter>('all');
|
||||
const [availabilityFilter, setAvailabilityFilter] = useState<AvailabilityFilter>('all');
|
||||
const [sortBy, setSortBy] = useState<SortOption>('rating');
|
||||
|
||||
// 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]);
|
||||
interface SponsorLeaguesTemplateProps {
|
||||
viewData: SponsorLeaguesViewData;
|
||||
filteredLeagues: AvailableLeague[];
|
||||
searchQuery: string;
|
||||
setSearchQuery: (query: string) => void;
|
||||
}
|
||||
|
||||
export function SponsorLeaguesTemplate({
|
||||
viewData,
|
||||
filteredLeagues,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
}: SponsorLeaguesTemplateProps) {
|
||||
const stats = viewData.stats;
|
||||
|
||||
return (
|
||||
@@ -138,12 +114,11 @@ export function SponsorLeaguesTemplate({ viewData }: SponsorLeaguesTemplateProps
|
||||
</Text>
|
||||
<Grid cols={4} gap={4}>
|
||||
<Box>
|
||||
<input
|
||||
type="text"
|
||||
<Input
|
||||
placeholder="Search leagues..."
|
||||
value={searchQuery}
|
||||
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>
|
||||
{/* Selects would go here, using standard Select UI if available */}
|
||||
@@ -175,7 +150,7 @@ export function SponsorLeaguesTemplate({ viewData }: SponsorLeaguesTemplateProps
|
||||
<Grid cols={3} gap={6}>
|
||||
{filteredLeagues.map((league) => (
|
||||
<GridItem key={league.id} colSpan={12} mdSpan={6} lgSpan={4}>
|
||||
<AvailableLeagueCard league={league as any} />
|
||||
<AvailableLeagueCard league={league} />
|
||||
</GridItem>
|
||||
))}
|
||||
</Grid>
|
||||
@@ -185,14 +160,12 @@ export function SponsorLeaguesTemplate({ viewData }: SponsorLeaguesTemplateProps
|
||||
<Surface variant="muted" rounded="full" padding={4}>
|
||||
<Icon icon={Trophy} size={12} color="#525252" />
|
||||
</Surface>
|
||||
<Box style={{ textAlign: 'center' }}>
|
||||
<Box textAlign="center">
|
||||
<Heading level={3}>No leagues found</Heading>
|
||||
<Text color="text-gray-400" block mt={2}>Try adjusting your filters to see more results</Text>
|
||||
</Box>
|
||||
<Button variant="secondary" onClick={() => {
|
||||
setSearchQuery('');
|
||||
setTierFilter('all');
|
||||
setAvailabilityFilter('all');
|
||||
}}>
|
||||
Clear Filters
|
||||
</Button>
|
||||
@@ -201,7 +174,7 @@ export function SponsorLeaguesTemplate({ viewData }: SponsorLeaguesTemplateProps
|
||||
)}
|
||||
|
||||
{/* 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}>
|
||||
<Icon icon={Megaphone} size={5} color="#3b82f6" />
|
||||
<Box>
|
||||
@@ -220,8 +193,8 @@ export function SponsorLeaguesTemplate({ viewData }: SponsorLeaguesTemplateProps
|
||||
function StatCard({ label, value, color = 'text-white' }: { label: string, value: string | number, color?: string }) {
|
||||
return (
|
||||
<Card>
|
||||
<Box style={{ textAlign: 'center' }}>
|
||||
<Text size="2xl" weight="bold" color={color as any} block mb={1}>{value}</Text>
|
||||
<Box textAlign="center">
|
||||
<Text size="2xl" weight="bold" color={color} block mb={1}>{value}</Text>
|
||||
<Text size="sm" color="text-gray-400">{label}</Text>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
@@ -55,7 +55,7 @@ export function SponsorshipRequestsTemplate({
|
||||
padding={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>
|
||||
{request.message && (
|
||||
<Text size="xs" color="text-gray-400" block mt={1}>{request.message}</Text>
|
||||
|
||||
@@ -30,9 +30,9 @@ export function StewardingTemplate({ viewData }: StewardingTemplateProps) {
|
||||
|
||||
{/* Stats summary */}
|
||||
<Grid cols={3} gap={4}>
|
||||
<StatItem label="Pending" value={viewData.totalPending} color="#f59e0b" />
|
||||
<StatItem label="Resolved" value={viewData.totalResolved} color="#10b981" />
|
||||
<StatItem label="Penalties" value={viewData.totalPenalties} color="#ef4444" />
|
||||
<StatItem label="Pending" value={viewData.totalPending} color="text-warning-amber" />
|
||||
<StatItem label="Resolved" value={viewData.totalResolved} color="text-performance-green" />
|
||||
<StatItem label="Penalties" value={viewData.totalPenalties} color="text-error-red" />
|
||||
</Grid>
|
||||
|
||||
{/* Content */}
|
||||
@@ -41,7 +41,7 @@ export function StewardingTemplate({ viewData }: StewardingTemplateProps) {
|
||||
<Surface variant="muted" rounded="full" padding={4}>
|
||||
<Icon icon={Flag} size={8} color="#10b981" />
|
||||
</Surface>
|
||||
<Box style={{ textAlign: 'center' }}>
|
||||
<Box textAlign="center">
|
||||
<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>
|
||||
</Box>
|
||||
@@ -54,10 +54,11 @@ export function StewardingTemplate({ viewData }: StewardingTemplateProps) {
|
||||
variant="muted"
|
||||
rounded="lg"
|
||||
border
|
||||
style={{ overflow: 'hidden', borderColor: '#262626' }}
|
||||
borderColor="border-neutral-800"
|
||||
overflow="hidden"
|
||||
>
|
||||
{/* 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={2}>
|
||||
<Icon icon={MapPin} size={4} color="#9ca3af" />
|
||||
@@ -67,7 +68,7 @@ export function StewardingTemplate({ viewData }: StewardingTemplateProps) {
|
||||
<Icon icon={Calendar} size={4} color="#9ca3af" />
|
||||
<Text size="sm" color="text-gray-400">{new Date(race.scheduledAt).toLocaleDateString()}</Text>
|
||||
</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>
|
||||
</Surface>
|
||||
</Stack>
|
||||
@@ -77,7 +78,7 @@ export function StewardingTemplate({ viewData }: StewardingTemplateProps) {
|
||||
<Box p={4}>
|
||||
{race.pendingProtests.length === 0 && race.resolvedProtests.length === 0 && race.penalties.length === 0 ? (
|
||||
<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>
|
||||
) : (
|
||||
<Stack gap={3}>
|
||||
@@ -92,16 +93,17 @@ export function StewardingTemplate({ viewData }: StewardingTemplateProps) {
|
||||
rounded="lg"
|
||||
border
|
||||
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}>
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Box flex={1} minWidth="0">
|
||||
<Stack direction="row" align="center" gap={2} mb={2} wrap>
|
||||
<Icon icon={AlertCircle} size={4} color="#f59e0b" />
|
||||
<Text weight="medium" color="text-white">
|
||||
{protester?.name || 'Unknown'} vs {accused?.name || 'Unknown'}
|
||||
</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>
|
||||
</Surface>
|
||||
</Stack>
|
||||
@@ -127,27 +129,28 @@ export function StewardingTemplate({ viewData }: StewardingTemplateProps) {
|
||||
rounded="lg"
|
||||
border
|
||||
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" 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" />
|
||||
</Surface>
|
||||
<Box>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<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' }}>
|
||||
<Text size="xs" weight="medium" color="text-error-red" style={{ textTransform: 'capitalize' }}>
|
||||
{penalty.type.replace('_', ' ')}
|
||||
<Surface variant="muted" rounded="full" padding={1} bg="bg-red-500/10" px={2}>
|
||||
<Text size="xs" weight="medium" color="text-error-red">
|
||||
{penalty.type.replace('_', ' ').toUpperCase()}
|
||||
</Text>
|
||||
</Surface>
|
||||
</Stack>
|
||||
<Text size="sm" color="text-gray-400" block mt={1}>{penalty.reason}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Box style={{ textAlign: 'right' }}>
|
||||
<Text weight="bold" color="text-error-red" style={{ fontSize: '1.125rem' }}>
|
||||
<Box textAlign="right">
|
||||
<Text weight="bold" color="text-error-red" size="lg">
|
||||
{penalty.type === 'time_penalty' && `+${penalty.value}s`}
|
||||
{penalty.type === 'grid_penalty' && `+${penalty.value} grid`}
|
||||
{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 }) {
|
||||
return (
|
||||
<Surface variant="muted" rounded="lg" border padding={4} style={{ backgroundColor: 'rgba(38, 38, 38, 0.5)', borderColor: '#262626', textAlign: 'center' }}>
|
||||
<Text size="2xl" weight="bold" style={{ color }}>{value}</Text>
|
||||
<Text size="sm" color="text-gray-500" block mt={1}>{label}</Text>
|
||||
<Surface variant="muted" rounded="lg" border padding={4} bg="bg-neutral-800/50" borderColor="border-neutral-800">
|
||||
<Box textAlign="center">
|
||||
<Text size="2xl" weight="bold" color={color}>{value}</Text>
|
||||
<Text size="sm" color="text-gray-400" block mt={1}>{label}</Text>
|
||||
</Box>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ export function TeamDetailTemplate({
|
||||
}: TeamDetailTemplateProps) {
|
||||
const isSponsorMode = useSponsorMode();
|
||||
|
||||
const team = viewData.team;
|
||||
|
||||
// Show loading state
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -61,7 +63,7 @@ export function TeamDetailTemplate({
|
||||
}
|
||||
|
||||
// Show not found state
|
||||
if (!viewData.team) {
|
||||
if (!team) {
|
||||
return (
|
||||
<Container size="md" py={12}>
|
||||
<Card>
|
||||
@@ -69,7 +71,7 @@ export function TeamDetailTemplate({
|
||||
<Box textAlign="center">
|
||||
<Heading level={1}>Team Not Found</Heading>
|
||||
<Text color="text-gray-400" block mt={2}>
|
||||
The team you're looking for doesn't exist or has been disbanded.
|
||||
The team you're looking for doesn't exist or has been disbanded.
|
||||
</Text>
|
||||
</Box>
|
||||
<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 (
|
||||
<Container size="lg" py={8}>
|
||||
<Stack gap={6}>
|
||||
@@ -98,16 +91,16 @@ export function TeamDetailTemplate({
|
||||
items={[
|
||||
{ label: 'Home', href: '/' },
|
||||
{ label: 'Teams', href: '/teams' },
|
||||
{ label: viewData.team.name }
|
||||
{ label: team.name }
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Sponsor Insights Card */}
|
||||
{isSponsorMode && viewData.team && (
|
||||
{isSponsorMode && team && (
|
||||
<SponsorInsightsCard
|
||||
entityType="team"
|
||||
entityId={viewData.team.id}
|
||||
entityName={viewData.team.name}
|
||||
entityId={team.id}
|
||||
entityName={team.name}
|
||||
tier="standard"
|
||||
metrics={viewData.teamMetrics}
|
||||
slots={SlotTemplates.team(true, true, 500, 250)}
|
||||
@@ -119,8 +112,8 @@ export function TeamDetailTemplate({
|
||||
|
||||
<TeamHero
|
||||
team={{
|
||||
...viewData.team,
|
||||
leagues: viewData.team.leagues.map(id => ({ id }))
|
||||
...team,
|
||||
leagues: team.leagues.map(id => ({ id }))
|
||||
}}
|
||||
memberCount={viewData.memberships.length}
|
||||
onUpdate={onUpdate}
|
||||
@@ -129,16 +122,20 @@ export function TeamDetailTemplate({
|
||||
{/* Tabs */}
|
||||
<Box borderBottom={true} borderColor="border-charcoal-outline">
|
||||
<Stack direction="row" gap={6}>
|
||||
{visibleTabs.map((tab) => (
|
||||
<Box
|
||||
key={tab.id}
|
||||
onClick={() => onTabChange(tab.id)}
|
||||
pb={3}
|
||||
cursor="pointer"
|
||||
className={`transition-all ${activeTab === tab.id ? 'border-b-2 border-primary-blue text-primary-blue' : 'border-b-2 border-transparent text-gray-400'}`}
|
||||
>
|
||||
<Text weight="medium">{tab.label}</Text>
|
||||
</Box>
|
||||
{viewData.tabs.map((tab) => (
|
||||
tab.visible && (
|
||||
<Box
|
||||
key={tab.id}
|
||||
onClick={() => onTabChange(tab.id)}
|
||||
pb={3}
|
||||
cursor="pointer"
|
||||
borderBottom={activeTab === tab.id ? '2px solid' : '2px solid'}
|
||||
borderColor={activeTab === tab.id ? 'border-primary-blue' : 'border-transparent'}
|
||||
color={activeTab === tab.id ? 'text-primary-blue' : 'text-gray-400'}
|
||||
>
|
||||
<Text weight="medium">{tab.label}</Text>
|
||||
</Box>
|
||||
)
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
@@ -152,7 +149,7 @@ export function TeamDetailTemplate({
|
||||
<Box mb={4}>
|
||||
<Heading level={2}>About</Heading>
|
||||
</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>
|
||||
</GridItem>
|
||||
|
||||
@@ -163,16 +160,16 @@ export function TeamDetailTemplate({
|
||||
</Box>
|
||||
<Stack gap={3}>
|
||||
<HorizontalStatItem label="Members" value={viewData.memberships.length.toString()} color="text-primary-blue" />
|
||||
{viewData.team.category && (
|
||||
<HorizontalStatItem label="Category" value={viewData.team.category} color="text-purple-400" />
|
||||
{team.category && (
|
||||
<HorizontalStatItem label="Category" value={team.category} color="text-purple-400" />
|
||||
)}
|
||||
{viewData.team.leagues && viewData.team.leagues.length > 0 && (
|
||||
<HorizontalStatItem label="Leagues" value={viewData.team.leagues.length.toString()} color="text-green-400" />
|
||||
{team.leagues && team.leagues.length > 0 && (
|
||||
<HorizontalStatItem label="Leagues" value={team.leagues.length.toString()} color="text-green-400" />
|
||||
)}
|
||||
{viewData.team.createdAt && (
|
||||
{team.createdAt && (
|
||||
<HorizontalStatItem
|
||||
label="Founded"
|
||||
value={new Date(viewData.team.createdAt).toLocaleDateString('en-US', {
|
||||
value={new Date(team.createdAt).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})}
|
||||
@@ -197,7 +194,7 @@ export function TeamDetailTemplate({
|
||||
|
||||
{activeTab === 'roster' && (
|
||||
<TeamRoster
|
||||
teamId={viewData.team.id}
|
||||
teamId={team.id}
|
||||
memberships={viewData.memberships}
|
||||
isAdmin={viewData.isAdmin}
|
||||
onRemoveMember={onRemoveMember}
|
||||
@@ -206,11 +203,11 @@ export function TeamDetailTemplate({
|
||||
)}
|
||||
|
||||
{activeTab === 'standings' && (
|
||||
<TeamStandings teamId={viewData.team.id} leagues={viewData.team.leagues} />
|
||||
<TeamStandings teamId={team.id} leagues={team.leagues} />
|
||||
)}
|
||||
|
||||
{activeTab === 'admin' && viewData.isAdmin && (
|
||||
<TeamAdmin team={viewData.team} onUpdate={onUpdate} />
|
||||
<TeamAdmin team={team} onUpdate={onUpdate} />
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import { Award, ArrowLeft } from 'lucide-react';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
@@ -11,18 +11,12 @@ import { Container } from '@/ui/Container';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { ModalIcon } from '@/ui/ModalIcon';
|
||||
import { TeamPodium } from '@/ui/TeamPodium';
|
||||
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
|
||||
import { TeamFilter } from '@/ui/TeamFilter';
|
||||
import { TeamRankingsTable } from '@/ui/TeamRankingsTable';
|
||||
|
||||
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
|
||||
type SortBy = 'rating' | 'wins' | 'winRate' | 'races';
|
||||
import type { TeamLeaderboardViewData, SkillLevel, SortBy } from '@/lib/view-data/TeamLeaderboardViewData';
|
||||
|
||||
interface TeamLeaderboardTemplateProps {
|
||||
teams: TeamSummaryViewModel[];
|
||||
searchQuery: string;
|
||||
filterLevel: SkillLevel | 'all';
|
||||
sortBy: SortBy;
|
||||
viewData: TeamLeaderboardViewData;
|
||||
onSearchChange: (query: string) => void;
|
||||
filterLevelChange: (level: SkillLevel | 'all') => void;
|
||||
onSortChange: (sort: SortBy) => void;
|
||||
@@ -31,40 +25,14 @@ interface TeamLeaderboardTemplateProps {
|
||||
}
|
||||
|
||||
export function TeamLeaderboardTemplate({
|
||||
teams,
|
||||
searchQuery,
|
||||
filterLevel,
|
||||
sortBy,
|
||||
viewData,
|
||||
onSearchChange,
|
||||
filterLevelChange,
|
||||
onSortChange,
|
||||
onTeamClick,
|
||||
onBackToTeams,
|
||||
}: TeamLeaderboardTemplateProps) {
|
||||
// Filter and sort teams
|
||||
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]);
|
||||
const { searchQuery, filterLevel, sortBy, filteredAndSortedTeams } = viewData;
|
||||
|
||||
return (
|
||||
<Container size="lg" py={8}>
|
||||
|
||||
@@ -20,6 +20,7 @@ import { Link } from '@/ui/Link';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { LoadingSpinner } from '@/ui/LoadingSpinner';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { ForgotPasswordViewData } from '@/lib/builders/view-data/types/ForgotPasswordViewData';
|
||||
|
||||
interface ForgotPasswordTemplateProps {
|
||||
@@ -37,14 +38,14 @@ interface ForgotPasswordTemplateProps {
|
||||
|
||||
export function ForgotPasswordTemplate({ viewData, formActions, mutationState }: ForgotPasswordTemplateProps) {
|
||||
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 */}
|
||||
<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 */}
|
||||
<Box style={{ 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' }}>
|
||||
<Box textAlign="center" mb={8}>
|
||||
<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" />
|
||||
</Surface>
|
||||
<Heading level={1}>Reset Password</Heading>
|
||||
@@ -53,20 +54,20 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Card style={{ position: 'relative', overflow: 'hidden' }}>
|
||||
<Card position="relative" overflow="hidden">
|
||||
{/* 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 ? (
|
||||
<form onSubmit={formActions.handleSubmit}>
|
||||
<Stack gap={5} style={{ position: 'relative' }}>
|
||||
<Box as="form" onSubmit={formActions.handleSubmit}>
|
||||
<Stack gap={5} position="relative">
|
||||
{/* Email */}
|
||||
<Box>
|
||||
<Text size="sm" weight="medium" color="text-gray-300" block mb={2}>
|
||||
Email Address
|
||||
</Text>
|
||||
<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" />
|
||||
</Box>
|
||||
<Input
|
||||
@@ -77,7 +78,6 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
|
||||
variant={viewData.formState.fields.email.error ? 'error' : 'default'}
|
||||
placeholder="you@example.com"
|
||||
disabled={mutationState.isPending}
|
||||
style={{ paddingLeft: '2.5rem' }}
|
||||
autoComplete="email"
|
||||
/>
|
||||
</Box>
|
||||
@@ -90,7 +90,7 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
|
||||
|
||||
{/* Error Message */}
|
||||
{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}>
|
||||
<Icon icon={AlertCircle} size={5} color="#ef4444" />
|
||||
<Text size="sm" color="text-error-red">{mutationState.error}</Text>
|
||||
@@ -110,8 +110,8 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
|
||||
</Button>
|
||||
|
||||
{/* Back to Login */}
|
||||
<Box style={{ textAlign: 'center' }}>
|
||||
<Link href="/auth/login">
|
||||
<Box textAlign="center">
|
||||
<Link href={routes.auth.login}>
|
||||
<Stack direction="row" align="center" justify="center" gap={1}>
|
||||
<Icon icon={ArrowLeft} size={4} color="#3b82f6" />
|
||||
<Text size="sm" color="text-primary-blue">Back to Login</Text>
|
||||
@@ -119,10 +119,10 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
|
||||
</Link>
|
||||
</Box>
|
||||
</Stack>
|
||||
</form>
|
||||
</Box>
|
||||
) : (
|
||||
<Stack gap={4} style={{ 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)' }}>
|
||||
<Stack gap={4} position="relative">
|
||||
<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}>
|
||||
<Icon icon={CheckCircle2} size={6} color="#10b981" />
|
||||
<Box>
|
||||
@@ -130,8 +130,8 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
|
||||
{viewData.magicLink && (
|
||||
<Box mt={2}>
|
||||
<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' }}>
|
||||
<Text size="xs" color="text-primary-blue" style={{ wordBreak: 'break-all' }}>{viewData.magicLink}</Text>
|
||||
<Surface variant="muted" rounded="md" border padding={2} bg="bg-neutral-800">
|
||||
<Text size="xs" color="text-primary-blue">{viewData.magicLink}</Text>
|
||||
</Surface>
|
||||
<Text size="xs" color="text-gray-500" block mt={1}>
|
||||
In production, this would be sent via email
|
||||
@@ -167,7 +167,7 @@ export function ForgotPasswordTemplate({ viewData, formActions, mutationState }:
|
||||
</Stack>
|
||||
|
||||
{/* Footer */}
|
||||
<Box mt={6} style={{ textAlign: 'center' }}>
|
||||
<Box mt={6} textAlign="center">
|
||||
<Text size="xs" color="text-gray-500">
|
||||
Need help?{' '}
|
||||
<Link href="/support">
|
||||
|
||||
@@ -25,6 +25,7 @@ import { LoadingSpinner } from '@/ui/LoadingSpinner';
|
||||
import { EnhancedFormError } from '@/components/errors/EnhancedFormError';
|
||||
import { UserRolesPreview } from '@/components/auth/UserRolesPreview';
|
||||
import { AuthWorkflowMockup } from '@/components/auth/AuthWorkflowMockup';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { LoginViewData } from '@/lib/builders/view-data/types/LoginViewData';
|
||||
import { FormState } from '@/lib/builders/view-data/types/FormState';
|
||||
|
||||
@@ -45,16 +46,16 @@ interface LoginTemplateProps {
|
||||
|
||||
export function LoginTemplate({ viewData, formActions, mutationState }: LoginTemplateProps) {
|
||||
return (
|
||||
<Box as="main" style={{ minHeight: '100vh', display: 'flex', position: 'relative' }}>
|
||||
<Box as="main" minHeight="100vh" display="flex" position="relative">
|
||||
{/* 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) */}
|
||||
<Box className="hidden lg:flex lg:w-1/2" style={{ position: 'relative', alignItems: 'center', justifyContent: 'center', padding: '3rem' }}>
|
||||
<Box style={{ maxWidth: '32rem' }}>
|
||||
<Box display={{ base: 'none', lg: 'flex' }} w={{ lg: '1/2' }} position="relative" alignItems="center" justifyContent="center" p={12}>
|
||||
<Box maxWidth="32rem">
|
||||
{/* Logo */}
|
||||
<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" />
|
||||
</Surface>
|
||||
<Text size="2xl" weight="bold" color="text-white">GridPilot</Text>
|
||||
@@ -90,11 +91,11 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
|
||||
</Box>
|
||||
|
||||
{/* Right Side - Login Form */}
|
||||
<Box style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '3rem 1rem', position: 'relative' }}>
|
||||
<Box style={{ width: '100%', maxWidth: '28rem' }}>
|
||||
<Box flex={1} display="flex" alignItems="center" justifyContent="center" p={{ base: 4, lg: 12 }} position="relative">
|
||||
<Box w="full" maxWidth="28rem">
|
||||
{/* Mobile Logo/Header */}
|
||||
<Box className="lg:hidden" style={{ 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' }}>
|
||||
<Box display={{ base: 'block', lg: 'none' }} textAlign="center" mb={8}>
|
||||
<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" />
|
||||
</Surface>
|
||||
<Heading level={1}>Welcome Back</Heading>
|
||||
@@ -104,26 +105,26 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
|
||||
</Box>
|
||||
|
||||
{/* 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>
|
||||
<Text color="text-gray-400" block mt={2}>
|
||||
Sign in to access your racing dashboard
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Card style={{ position: 'relative', overflow: 'hidden' }}>
|
||||
<Card position="relative" overflow="hidden">
|
||||
{/* 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}>
|
||||
<Stack gap={5} style={{ position: 'relative' }}>
|
||||
<Box as="form" onSubmit={formActions.handleSubmit}>
|
||||
<Stack gap={5} position="relative">
|
||||
{/* Email */}
|
||||
<Box>
|
||||
<Text size="sm" weight="medium" color="text-gray-300" block mb={2}>
|
||||
Email Address
|
||||
</Text>
|
||||
<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" />
|
||||
</Box>
|
||||
<Input
|
||||
@@ -135,7 +136,6 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
|
||||
variant={viewData.formState.fields.email.error ? 'error' : 'default'}
|
||||
placeholder="you@example.com"
|
||||
disabled={viewData.formState.isSubmitting || mutationState.isPending}
|
||||
style={{ paddingLeft: '2.5rem' }}
|
||||
autoComplete="email"
|
||||
/>
|
||||
</Box>
|
||||
@@ -152,12 +152,12 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
|
||||
<Text size="sm" weight="medium" color="text-gray-300">
|
||||
Password
|
||||
</Text>
|
||||
<Link href="/auth/forgot-password">
|
||||
<Link href={routes.auth.forgotPassword}>
|
||||
<Text size="xs" color="text-primary-blue">Forgot password?</Text>
|
||||
</Link>
|
||||
</Stack>
|
||||
<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" />
|
||||
</Box>
|
||||
<Input
|
||||
@@ -169,14 +169,19 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
|
||||
variant={viewData.formState.fields.password.error ? 'error' : 'default'}
|
||||
placeholder="••••••••"
|
||||
disabled={viewData.formState.isSubmitting || mutationState.isPending}
|
||||
style={{ paddingLeft: '2.5rem', paddingRight: '2.5rem' }}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<Box
|
||||
as="button"
|
||||
type="button"
|
||||
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" />
|
||||
</Box>
|
||||
@@ -190,27 +195,27 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
|
||||
|
||||
{/* Remember Me */}
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<input
|
||||
<Box
|
||||
as="input"
|
||||
id="rememberMe"
|
||||
name="rememberMe"
|
||||
type="checkbox"
|
||||
checked={viewData.formState.fields.rememberMe.value as boolean}
|
||||
onChange={formActions.handleChange}
|
||||
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>
|
||||
</Stack>
|
||||
|
||||
{/* Insufficient Permissions Message */}
|
||||
{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}>
|
||||
<Icon icon={AlertCircle} size={5} color="#f59e0b" />
|
||||
<Box>
|
||||
<Text weight="bold" color="text-warning-amber" block>Insufficient Permissions</Text>
|
||||
<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't have permission to access that page. Please log in with an account that has the required role.
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
@@ -239,24 +244,24 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
|
||||
{mutationState.isPending || viewData.formState.isSubmitting ? 'Signing in...' : 'Sign In'}
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Box>
|
||||
|
||||
{/* Divider */}
|
||||
<Box style={{ position: 'relative' }} my={6}>
|
||||
<Box style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center' }}>
|
||||
<Box style={{ width: '100%', borderTop: '1px solid #262626' }} />
|
||||
<Box position="relative" my={6}>
|
||||
<Box position="absolute" inset="0" display="flex" alignItems="center">
|
||||
<Box w="full" borderTop borderColor="border-neutral-800" />
|
||||
</Box>
|
||||
<Box style={{ position: 'relative', display: 'flex', justifyContent: 'center' }}>
|
||||
<Box px={4} style={{ backgroundColor: '#171717' }}>
|
||||
<Box position="relative" display="flex" justifyContent="center">
|
||||
<Box px={4} bg="bg-neutral-900">
|
||||
<Text size="xs" color="text-gray-500">or continue with</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Sign Up Link */}
|
||||
<Box style={{ textAlign: 'center' }} mt={6}>
|
||||
<Box textAlign="center" mt={6}>
|
||||
<Text size="sm" color="text-gray-400">
|
||||
Don't have an account?{' '}
|
||||
Don't have an account?{' '}
|
||||
<Link
|
||||
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 */}
|
||||
<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}>
|
||||
<Icon icon={AlertCircle} size={5} color="#737373" />
|
||||
<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's correct when creating your account.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Surface>
|
||||
</Box>
|
||||
|
||||
{/* Footer */}
|
||||
<Box mt={6} style={{ textAlign: 'center' }}>
|
||||
<Box mt={6} textAlign="center">
|
||||
<Text size="xs" color="text-gray-500">
|
||||
By signing in, you agree to our{' '}
|
||||
<Link href="/terms">
|
||||
@@ -293,7 +298,7 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
|
||||
</Box>
|
||||
|
||||
{/* Mobile Role Info */}
|
||||
<Box mt={8} className="lg:hidden">
|
||||
<Box mt={8} display={{ base: 'block', lg: 'none' }}>
|
||||
<UserRolesPreview variant="compact" />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -22,9 +22,11 @@ import { Link } from '@/ui/Link';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { LoadingSpinner } from '@/ui/LoadingSpinner';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { ResetPasswordViewData } from '@/lib/builders/view-data/types/ResetPasswordViewData';
|
||||
|
||||
interface ResetPasswordTemplateProps extends ResetPasswordViewData {
|
||||
interface ResetPasswordTemplateProps {
|
||||
viewData: ResetPasswordViewData;
|
||||
formActions: {
|
||||
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => Promise<void>;
|
||||
@@ -42,18 +44,21 @@ interface ResetPasswordTemplateProps extends ResetPasswordViewData {
|
||||
};
|
||||
}
|
||||
|
||||
export function ResetPasswordTemplate(props: ResetPasswordTemplateProps) {
|
||||
const { formActions, uiState, mutationState, ...viewData } = props;
|
||||
|
||||
export function ResetPasswordTemplate({
|
||||
viewData,
|
||||
formActions,
|
||||
uiState,
|
||||
mutationState,
|
||||
}: ResetPasswordTemplateProps) {
|
||||
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 */}
|
||||
<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 */}
|
||||
<Box style={{ 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' }}>
|
||||
<Box textAlign="center" mb={8}>
|
||||
<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" />
|
||||
</Surface>
|
||||
<Heading level={1}>Reset Password</Heading>
|
||||
@@ -62,20 +67,20 @@ export function ResetPasswordTemplate(props: ResetPasswordTemplateProps) {
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Card style={{ position: 'relative', overflow: 'hidden' }}>
|
||||
<Card position="relative" overflow="hidden">
|
||||
{/* 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 ? (
|
||||
<form onSubmit={formActions.handleSubmit}>
|
||||
<Stack gap={5} style={{ position: 'relative' }}>
|
||||
<Box as="form" onSubmit={formActions.handleSubmit}>
|
||||
<Stack gap={5} position="relative">
|
||||
{/* New Password */}
|
||||
<Box>
|
||||
<Text size="sm" weight="medium" color="text-gray-300" block mb={2}>
|
||||
New Password
|
||||
</Text>
|
||||
<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" />
|
||||
</Box>
|
||||
<Input
|
||||
@@ -87,14 +92,19 @@ export function ResetPasswordTemplate(props: ResetPasswordTemplateProps) {
|
||||
variant={viewData.formState.fields.newPassword.error ? 'error' : 'default'}
|
||||
placeholder="••••••••"
|
||||
disabled={mutationState.isPending}
|
||||
style={{ paddingLeft: '2.5rem', paddingRight: '2.5rem' }}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<Box
|
||||
as="button"
|
||||
type="button"
|
||||
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" />
|
||||
</Box>
|
||||
@@ -112,7 +122,7 @@ export function ResetPasswordTemplate(props: ResetPasswordTemplateProps) {
|
||||
Confirm Password
|
||||
</Text>
|
||||
<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" />
|
||||
</Box>
|
||||
<Input
|
||||
@@ -124,14 +134,19 @@ export function ResetPasswordTemplate(props: ResetPasswordTemplateProps) {
|
||||
variant={viewData.formState.fields.confirmPassword.error ? 'error' : 'default'}
|
||||
placeholder="••••••••"
|
||||
disabled={mutationState.isPending}
|
||||
style={{ paddingLeft: '2.5rem', paddingRight: '2.5rem' }}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<Box
|
||||
as="button"
|
||||
type="button"
|
||||
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" />
|
||||
</Box>
|
||||
@@ -145,7 +160,7 @@ export function ResetPasswordTemplate(props: ResetPasswordTemplateProps) {
|
||||
|
||||
{/* Error Message */}
|
||||
{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}>
|
||||
<Icon icon={AlertCircle} size={5} color="#ef4444" />
|
||||
<Text size="sm" color="text-error-red">{mutationState.error}</Text>
|
||||
@@ -165,8 +180,8 @@ export function ResetPasswordTemplate(props: ResetPasswordTemplateProps) {
|
||||
</Button>
|
||||
|
||||
{/* Back to Login */}
|
||||
<Box style={{ textAlign: 'center' }}>
|
||||
<Link href="/auth/login">
|
||||
<Box textAlign="center">
|
||||
<Link href={routes.auth.login}>
|
||||
<Stack direction="row" align="center" justify="center" gap={1}>
|
||||
<Icon icon={ArrowLeft} size={4} color="#3b82f6" />
|
||||
<Text size="sm" color="text-primary-blue">Back to Login</Text>
|
||||
@@ -174,10 +189,10 @@ export function ResetPasswordTemplate(props: ResetPasswordTemplateProps) {
|
||||
</Link>
|
||||
</Box>
|
||||
</Stack>
|
||||
</form>
|
||||
</Box>
|
||||
) : (
|
||||
<Stack gap={4} style={{ 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)' }}>
|
||||
<Stack gap={4} position="relative">
|
||||
<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}>
|
||||
<Icon icon={CheckCircle2} size={6} color="#10b981" />
|
||||
<Box>
|
||||
@@ -214,7 +229,7 @@ export function ResetPasswordTemplate(props: ResetPasswordTemplateProps) {
|
||||
</Stack>
|
||||
|
||||
{/* Footer */}
|
||||
<Box mt={6} style={{ textAlign: 'center' }}>
|
||||
<Box mt={6} textAlign="center">
|
||||
<Text size="xs" color="text-gray-500">
|
||||
Need help?{' '}
|
||||
<Link href="/support">
|
||||
|
||||
@@ -56,18 +56,21 @@ const USER_ROLES = [
|
||||
title: 'Driver',
|
||||
description: 'Race, track stats, join teams',
|
||||
color: '#3b82f6',
|
||||
bg: 'bg-blue-500/10',
|
||||
},
|
||||
{
|
||||
icon: Trophy,
|
||||
title: 'League Admin',
|
||||
description: 'Organize leagues and events',
|
||||
color: '#10b981',
|
||||
bg: 'bg-green-500/10',
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: 'Team Manager',
|
||||
description: 'Manage team and drivers',
|
||||
color: '#a855f7',
|
||||
bg: 'bg-purple-500/10',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -91,16 +94,16 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
];
|
||||
|
||||
return (
|
||||
<Box as="main" style={{ minHeight: '100vh', display: 'flex', position: 'relative' }}>
|
||||
<Box as="main" minHeight="100vh" display="flex" position="relative">
|
||||
{/* 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) */}
|
||||
<Box className="hidden lg:flex lg:w-1/2" style={{ position: 'relative', alignItems: 'center', justifyContent: 'center', padding: '3rem' }}>
|
||||
<Box style={{ maxWidth: '32rem' }}>
|
||||
<Box display={{ base: 'none', lg: 'flex' }} w={{ lg: '1/2' }} position="relative" alignItems="center" justifyContent="center" p={12}>
|
||||
<Box maxWidth="32rem">
|
||||
{/* Logo */}
|
||||
<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" />
|
||||
</Surface>
|
||||
<Text size="2xl" weight="bold" color="text-white">GridPilot</Text>
|
||||
@@ -123,10 +126,11 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
rounded="xl"
|
||||
border
|
||||
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}>
|
||||
<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} />
|
||||
</Surface>
|
||||
<Box>
|
||||
@@ -140,7 +144,7 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
|
||||
{/* Features List */}
|
||||
<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}>
|
||||
<Icon icon={Sparkles} size={4} color="#3b82f6" />
|
||||
<Text size="sm" weight="medium" color="text-white">What you'll get</Text>
|
||||
@@ -168,11 +172,11 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
</Box>
|
||||
|
||||
{/* Right Side - Signup Form */}
|
||||
<Box style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '3rem 1rem', position: 'relative', overflowY: 'auto' }}>
|
||||
<Box style={{ width: '100%', maxWidth: '28rem' }}>
|
||||
<Box flex={1} display="flex" alignItems="center" justifyContent="center" p={{ base: 4, lg: 12 }} position="relative" overflow="auto">
|
||||
<Box w="full" maxWidth="28rem">
|
||||
{/* Mobile Logo/Header */}
|
||||
<Box className="lg:hidden" style={{ 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' }}>
|
||||
<Box display={{ base: 'block', lg: 'none' }} textAlign="center" mb={8}>
|
||||
<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" />
|
||||
</Surface>
|
||||
<Heading level={1}>Join GridPilot</Heading>
|
||||
@@ -182,26 +186,26 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
</Box>
|
||||
|
||||
{/* 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>
|
||||
<Text color="text-gray-400" block mt={2}>
|
||||
Get started with your free account
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Card style={{ position: 'relative', overflow: 'hidden' }}>
|
||||
<Card position="relative" overflow="hidden">
|
||||
{/* 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}>
|
||||
<Stack gap={4} style={{ position: 'relative' }}>
|
||||
<Box as="form" onSubmit={formActions.handleSubmit}>
|
||||
<Stack gap={4} position="relative">
|
||||
{/* First Name */}
|
||||
<Box>
|
||||
<Text size="sm" weight="medium" color="text-gray-300" block mb={2}>
|
||||
First Name
|
||||
</Text>
|
||||
<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" />
|
||||
</Box>
|
||||
<Input
|
||||
@@ -213,7 +217,6 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
variant={viewData.formState.fields.firstName.error ? 'error' : 'default'}
|
||||
placeholder="John"
|
||||
disabled={mutationState.isPending}
|
||||
style={{ paddingLeft: '2.5rem' }}
|
||||
autoComplete="given-name"
|
||||
/>
|
||||
</Box>
|
||||
@@ -230,7 +233,7 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
Last Name
|
||||
</Text>
|
||||
<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" />
|
||||
</Box>
|
||||
<Input
|
||||
@@ -242,7 +245,6 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
variant={viewData.formState.fields.lastName.error ? 'error' : 'default'}
|
||||
placeholder="Smith"
|
||||
disabled={mutationState.isPending}
|
||||
style={{ paddingLeft: '2.5rem' }}
|
||||
autoComplete="family-name"
|
||||
/>
|
||||
</Box>
|
||||
@@ -255,11 +257,11 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
</Box>
|
||||
|
||||
{/* 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}>
|
||||
<Icon icon={AlertCircle} size={5} color="#f59e0b" />
|
||||
<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's correct.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Surface>
|
||||
@@ -270,7 +272,7 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
Email Address
|
||||
</Text>
|
||||
<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" />
|
||||
</Box>
|
||||
<Input
|
||||
@@ -282,7 +284,6 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
variant={viewData.formState.fields.email.error ? 'error' : 'default'}
|
||||
placeholder="you@example.com"
|
||||
disabled={mutationState.isPending}
|
||||
style={{ paddingLeft: '2.5rem' }}
|
||||
autoComplete="email"
|
||||
/>
|
||||
</Box>
|
||||
@@ -299,7 +300,7 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
Password
|
||||
</Text>
|
||||
<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" />
|
||||
</Box>
|
||||
<Input
|
||||
@@ -311,14 +312,19 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
variant={viewData.formState.fields.password.error ? 'error' : 'default'}
|
||||
placeholder="••••••••"
|
||||
disabled={mutationState.isPending}
|
||||
style={{ paddingLeft: '2.5rem', paddingRight: '2.5rem' }}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<Box
|
||||
as="button"
|
||||
type="button"
|
||||
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" />
|
||||
</Box>
|
||||
@@ -333,14 +339,14 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
{viewData.formState.fields.password.value && (
|
||||
<Box mt={3}>
|
||||
<Stack direction="row" align="center" gap={2} mb={2}>
|
||||
<Box style={{ flex: 1, height: '0.375rem', borderRadius: '9999px', backgroundColor: '#262626', 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 flex={1} h="1.5" rounded="full" bg="bg-neutral-800" overflow="hidden">
|
||||
<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>
|
||||
<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}
|
||||
</Text>
|
||||
</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) => (
|
||||
<Stack key={index} direction="row" align="center" gap={1.5}>
|
||||
<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
|
||||
</Text>
|
||||
<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" />
|
||||
</Box>
|
||||
<Input
|
||||
@@ -372,14 +378,19 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
variant={viewData.formState.fields.confirmPassword.error ? 'error' : 'default'}
|
||||
placeholder="••••••••"
|
||||
disabled={mutationState.isPending}
|
||||
style={{ paddingLeft: '2.5rem', paddingRight: '2.5rem' }}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<Box
|
||||
as="button"
|
||||
type="button"
|
||||
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" />
|
||||
</Box>
|
||||
@@ -408,22 +419,22 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
{mutationState.isPending ? 'Creating account...' : 'Create Account'}
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Box>
|
||||
|
||||
{/* Divider */}
|
||||
<Box style={{ position: 'relative' }} my={6}>
|
||||
<Box style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center' }}>
|
||||
<Box style={{ width: '100%', borderTop: '1px solid #262626' }} />
|
||||
<Box position="relative" my={6}>
|
||||
<Box position="absolute" inset="0" display="flex" alignItems="center">
|
||||
<Box w="full" borderTop borderColor="border-neutral-800" />
|
||||
</Box>
|
||||
<Box style={{ position: 'relative', display: 'flex', justifyContent: 'center' }}>
|
||||
<Box px={4} style={{ backgroundColor: '#171717' }}>
|
||||
<Box position="relative" display="flex" justifyContent="center">
|
||||
<Box px={4} bg="bg-neutral-900">
|
||||
<Text size="xs" color="text-gray-500">or continue with</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Login Link */}
|
||||
<Box style={{ textAlign: 'center' }} mt={6}>
|
||||
<Box textAlign="center" mt={6}>
|
||||
<Text size="sm" color="text-gray-400">
|
||||
Already have an account?{' '}
|
||||
<Link
|
||||
@@ -436,7 +447,7 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
</Card>
|
||||
|
||||
{/* Footer */}
|
||||
<Box mt={6} style={{ textAlign: 'center' }}>
|
||||
<Box mt={6} textAlign="center">
|
||||
<Text size="xs" color="text-gray-500">
|
||||
By creating an account, you agree to our{' '}
|
||||
<Link href="/terms">
|
||||
@@ -450,12 +461,12 @@ export function SignupTemplate({ viewData, formActions, uiState, mutationState }
|
||||
</Box>
|
||||
|
||||
{/* Mobile Role Info */}
|
||||
<Box mt={8} className="lg:hidden">
|
||||
<Text size="xs" color="text-gray-500" block mb={4} style={{ textAlign: 'center' }}>One account for all roles</Text>
|
||||
<Box mt={8} display={{ base: 'block', lg: 'none' }}>
|
||||
<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}>
|
||||
{USER_ROLES.map((role) => (
|
||||
<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} />
|
||||
</Surface>
|
||||
<Text size="xs" color="text-gray-500">{role.title}</Text>
|
||||
|
||||
@@ -11,8 +11,8 @@ interface Race {
|
||||
scheduledAt: string;
|
||||
status: 'scheduled' | 'running' | 'completed' | 'cancelled';
|
||||
sessionType: string;
|
||||
leagueId?: string;
|
||||
leagueName?: string;
|
||||
leagueId?: string | null;
|
||||
leagueName?: string | null;
|
||||
strengthOfField?: number | null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user