website refactor

This commit is contained in:
2026-01-18 13:26:35 +01:00
parent 350c78504d
commit 0b301feb61
225 changed files with 1678 additions and 26666 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,13 @@
# API Smoke Test Report
**Generated:** 2026-01-08T18:44:12.182Z
**Generated:** 2026-01-18T00:40:18.011Z
**API Base URL:** http://localhost:3101
## Summary
- **Total Endpoints:** 31
- **✅ Success:** 30
- **❌ Failed:** 1
- **Total Endpoints:** 0
- **✅ Success:** 0
- **❌ Failed:** 0
- **⚠️ Presenter Errors:** 0
- **Avg Response Time:** 24.65ms
## Other Failures
1. **GET /payments/wallets?leagueId=a09b8755-d584-47b8-b725-c86fb261bb6b**
- Status: 403
- Error: Forbidden
- **Avg Response Time:** 0.00ms

View File

@@ -1,6 +1,7 @@
import { notFound, redirect } from 'next/navigation';
import { DashboardPageQuery } from '@/lib/page-queries/DashboardPageQuery';
import { DashboardTemplate } from '@/templates/DashboardTemplate';
import { logger } from '@/lib/infrastructure/logging/logger';
export default async function DashboardPage() {
const result = await DashboardPageQuery.execute();
@@ -15,7 +16,7 @@ export default async function DashboardPage() {
redirect('/');
} else {
// serverError, networkError, unknown, validationError, unauthorized
console.error('Dashboard error:', error);
logger.error('Dashboard error', undefined, { errorType: error });
notFound();
}
}

View File

@@ -0,0 +1,5 @@
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({ status: 'ok', timestamp: new Date().toISOString() });
}

View File

@@ -6,6 +6,8 @@ import React from 'react';
import './globals.css';
import { AppWrapper } from '@/components/AppWrapper';
import { RootAppShellTemplate } from '@/templates/layout/RootAppShellTemplate';
import { getWebsiteServerEnv } from '@/lib/config/env';
import { logger } from '@/lib/infrastructure/logging/logger';
export const dynamic = 'force-dynamic';
@@ -46,7 +48,8 @@ export default async function RootLayout({
children: React.ReactNode;
}) {
// Initialize debug tools in development
if (process.env.NODE_ENV === 'development') {
const env = getWebsiteServerEnv();
if (env.NODE_ENV === 'development') {
try {
initializeGlobalErrorHandling({
showDevOverlay: true,
@@ -59,7 +62,7 @@ export default async function RootLayout({
logResponses: true,
});
} catch (error) {
console.warn('Failed to initialize debug tools:', error);
logger.warn('Failed to initialize debug tools', { error });
}
}

View File

@@ -2,6 +2,7 @@ import { notFound, redirect } from 'next/navigation';
import { DriverRankingsPageQuery } from '@/lib/page-queries/DriverRankingsPageQuery';
import { DriverRankingsPageClient } from './DriverRankingsPageClient';
import { routes } from '@/lib/routing/RouteConfig';
import { logger } from '@/lib/infrastructure/logging/logger';
export default async function DriverLeaderboardPage() {
const result = await DriverRankingsPageQuery.execute();
@@ -16,7 +17,7 @@ export default async function DriverLeaderboardPage() {
redirect(routes.public.home);
} else {
// serverError, networkError, unknown, validationError, unauthorized
console.error('Driver rankings error:', error);
logger.error('Driver rankings error:', undefined, { error });
notFound();
}
}

View File

@@ -2,6 +2,7 @@ import { notFound, redirect } from 'next/navigation';
import { LeaderboardsPageQuery } from '@/lib/page-queries/LeaderboardsPageQuery';
import { LeaderboardsPageClient } from './LeaderboardsPageClient';
import { routes } from '@/lib/routing/RouteConfig';
import { logger } from '@/lib/infrastructure/logging/logger';
export default async function LeaderboardsPage() {
const result = await LeaderboardsPageQuery.execute();
@@ -16,7 +17,7 @@ export default async function LeaderboardsPage() {
redirect(routes.public.home);
} else {
// serverError, networkError, unknown, validationError, unauthorized
console.error('Leaderboards error:', error);
logger.error('Leaderboards error:', undefined, { error });
notFound();
}
}

View File

@@ -1,7 +1,8 @@
'use client';
import React, { useState } from 'react';
import { LeagueCard } from '@/components/leagues/LeagueCard';
import { LeagueCard } from '@/components/leagues/LeagueCardWrapper';
import { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
import { routes } from '@/lib/routing/RouteConfig';
import type { LeaguesViewData } from '@/lib/view-data/LeaguesViewData';
import { Box } from '@/ui/Box';
@@ -26,7 +27,6 @@ import {
type LucideIcon,
} from 'lucide-react';
import { useRouter } from 'next/navigation';
import { getMediaUrl } from '@/lib/utilities/media';
// ============================================================================
// TYPES
@@ -252,15 +252,7 @@ export function LeaguesPageClient({ viewData }: LeaguesTemplateProps) {
{filteredLeagues.map((league) => (
<LeagueCard
key={league.id}
id={league.id}
name={league.name}
description={league.description || undefined}
coverUrl={getMediaUrl('league-cover', league.id)}
logoUrl={league.logoUrl || undefined}
gameName={league.scoring?.gameName}
memberCount={league.usedDriverSlots || 0}
maxMembers={league.maxDrivers}
championshipType={(league.scoring?.primaryChampionshipType as 'driver' | 'team' | 'nations' | 'trophy') || 'driver'}
league={league as unknown as LeagueSummaryViewModel}
onClick={() => router.push(routes.league.detail(league.id))}
/>
))}

View File

@@ -2,7 +2,7 @@
import React, { useState } from 'react';
import { LeagueRulebookTemplate } from '@/templates/LeagueRulebookTemplate';
import { type RulebookSection } from '@/ui/RulebookTabs';
import { type RulebookSection } from '@/components/leagues/RulebookTabs';
import type { LeagueRulebookViewData } from '@/lib/view-data/LeagueRulebookViewData';
interface LeagueRulebookPageClientProps {

View File

@@ -1,7 +1,7 @@
'use client';
import { StewardingQueuePanel } from '@/components/leagues/StewardingQueuePanel';
import { PenaltyFAB } from '@/ui/PenaltyFAB';
import { PenaltyFAB } from '@/components/races/PenaltyFAB';
import { QuickPenaltyModal } from '@/components/leagues/QuickPenaltyModal';
import { ReviewProtestModal } from '@/components/leagues/ReviewProtestModal';
import { StewardingStats } from '@/components/leagues/StewardingStats';

View File

@@ -1,6 +1,7 @@
'use client';
import { useState, useMemo } from 'react';
import { useRouter } from 'next/navigation';
import { RacesTemplate, type TimeFilter, type RaceStatusFilter } from '@/templates/RacesTemplate';
import type { RacesViewData } from '@/lib/view-data/RacesViewData';
@@ -9,6 +10,7 @@ interface RacesPageClientProps {
}
export function RacesPageClient({ viewData }: RacesPageClientProps) {
const router = useRouter();
const [statusFilter, setStatusFilter] = useState<RaceStatusFilter>('all');
const [leagueFilter, setLeagueFilter] = useState<string>('all');
const [timeFilter, setTimeFilter] = useState<TimeFilter>('upcoming');
@@ -56,8 +58,8 @@ export function RacesPageClient({ viewData }: RacesPageClientProps) {
setTimeFilter={setTimeFilter}
showFilterModal={showFilterModal}
setShowFilterModal={setShowFilterModal}
onRaceClick={(id) => console.log('Race click', id)}
onLeagueClick={(id) => console.log('League click', id)}
onRaceClick={(id) => router.push(`/races/${id}`)}
onLeagueClick={(id) => router.push(`/leagues/${id}`)}
onWithdraw={(id) => console.log('Withdraw', id)}
onCancel={(id) => console.log('Cancel', id)}
/>

View File

@@ -1,6 +1,7 @@
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';
import { createRouteGuard } from '@/lib/auth/createRouteGuard';
import { Box } from '@/ui/Box';
interface SponsorLayoutProps {
children: React.ReactNode;
@@ -23,8 +24,8 @@ export default async function SponsorLayout({ children }: SponsorLayoutProps) {
}
return (
<div className="min-h-screen bg-deep-graphite">
<Box minHeight="screen" bg="bg-deep-graphite">
{children}
</div>
</Box>
);
}

View File

@@ -3,7 +3,7 @@
import React, { useState, useMemo } from 'react';
import { SponsorLeaguesTemplate, type SortOption, type TierFilter, type AvailabilityFilter } from '@/templates/SponsorLeaguesTemplate';
export default function SponsorLeaguesPageClient({ data }: { data: unknown }) {
export function SponsorLeaguesPageClient({ data }: { data: unknown }) {
const [searchQuery, setSearchQuery] = useState('');
const [tierFilter] = useState<TierFilter>('all');
const [availabilityFilter] = useState<AvailabilityFilter>('all');

View File

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

View File

@@ -1,20 +1,22 @@
import { notFound } from 'next/navigation';
import { PageWrapper } from '@/components/shared/state/PageWrapper';
import SponsorLeagueDetailPageClient from './SponsorLeagueDetailPageClient';
import { SponsorLeagueDetailPageClient } from './SponsorLeagueDetailPageClient';
import { SponsorsApiClient } from '@/lib/api/sponsors/SponsorsApiClient';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { getWebsiteServerEnv } from '@/lib/config/env';
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
// Manual wiring: create dependencies
const baseUrl = getWebsiteApiBaseUrl();
const env = getWebsiteServerEnv();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
reportToExternal: env.NODE_ENV === 'production',
});
// Create API client

View File

@@ -1,18 +1,20 @@
import { PageWrapper } from '@/components/shared/state/PageWrapper';
import SponsorLeaguesPageClient from './SponsorLeaguesPageClient';
import { SponsorLeaguesPageClient } from './SponsorLeaguesPageClient';
import { SponsorsApiClient } from '@/lib/api/sponsors/SponsorsApiClient';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { getWebsiteServerEnv } from '@/lib/config/env';
export default async function Page() {
// Manual wiring: create dependencies
const baseUrl = getWebsiteApiBaseUrl();
const env = getWebsiteServerEnv();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
reportToExternal: env.NODE_ENV === 'production',
});
// Create API client

View File

@@ -5,6 +5,8 @@ import { SponsorSettingsTemplate } from '@/templates/SponsorSettingsTemplate';
import { logoutAction } from '@/app/actions/logoutAction';
import { ConfirmDialog } from '@/components/shared/ux/ConfirmDialog';
import { useRouter } from 'next/navigation';
import { routes } from '@/lib/routing/RouteConfig';
import { logger } from '@/lib/infrastructure/logging/logger';
// ============================================================================
// Mock Data
@@ -61,7 +63,7 @@ export default function SponsorSettingsPage() {
const handleSaveProfile = async () => {
setSaving(true);
await new Promise(resolve => setTimeout(resolve, 800));
console.log('Profile saved:', profile);
logger.info('Profile saved', { profile });
setSaving(false);
setSaved(true);
setTimeout(() => setSaved(false), 3000);
@@ -71,11 +73,11 @@ export default function SponsorSettingsPage() {
setIsDeleting(true);
const result = await logoutAction();
if (result.isErr()) {
console.error('Logout failed:', result.getError());
logger.error('Logout failed', new Error(result.getError()));
setIsDeleting(false);
return;
}
router.push('/auth/login');
router.push(routes.auth.login);
};
const viewData = {

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { Box } from './Box';
import { Text } from './Text';
import { Stack } from './Stack';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
interface AchievementCardProps {
title: string;

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Box } from './Box';
import { Text } from './Text';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
interface MilestoneItemProps {
label: string;

View File

@@ -1,8 +1,8 @@
import { Box } from './Box';
import { Image } from './Image';
import { Text } from './Text';
import { Box } from '@/ui/Box';
import { Image } from '@/ui/Image';
import { Text } from '@/ui/Text';
interface ActiveDriverCardProps {
name: string;

View File

@@ -1,11 +1,11 @@
import { AchievementCard } from '@/ui/AchievementCard';
import { AchievementCard } from '@/components/achievements/AchievementCard';
import { Box } from '@/ui/Box';
import { Card } from '@/ui/Card';
import { GoalCard } from '@/ui/GoalCard';
import { Heading } from '@/ui/Heading';
import { MilestoneItem } from '@/ui/MilestoneItem';
import { MilestoneItem } from '@/components/achievements/MilestoneItem';
import { Stack } from '@/ui/Stack';
interface Achievement {

View File

@@ -1,10 +1,10 @@
import React from 'react';
import { Card } from '@/ui/Card';
import { RankBadge } from '@/ui/RankBadge';
import { RankBadge } from '@/components/leaderboards/RankBadge';
import { DriverIdentity } from '@/components/drivers/DriverIdentity';
import { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import { Stack } from '@/ui/Stack';
import { DriverStats } from '@/ui/DriverStats';
import { DriverStats } from '@/components/drivers/DriverStats';
import { routes } from '@/lib/routing/RouteConfig';
export interface DriverCardProps {

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { Box } from './Box';
import { Text } from './Text';
import { Stack } from './Stack';
import { Image } from './Image';
import { RatingBadge } from './RatingBadge';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Image } from '@/ui/Image';
import { RatingBadge } from '@/components/drivers/RatingBadge';
interface DriverHeaderPanelProps {
name: string;

View File

@@ -9,9 +9,9 @@ import { Stack } from '@/ui/Stack';
import { StatCard } from '@/ui/StatCard';
import { ProfileHeader } from '@/components/drivers/ProfileHeader';
import { ProfileStats } from './ProfileStats';
import { CareerHighlights } from '@/ui/CareerHighlights';
import { CareerHighlights } from '@/components/drivers/CareerHighlights';
import { DriverRankings } from '@/components/drivers/DriverRankings';
import { PerformanceMetrics } from '@/ui/PerformanceMetrics';
import { PerformanceMetrics } from '@/components/drivers/PerformanceMetrics';
import { useDriverProfile } from "@/hooks/driver/useDriverProfile";
interface DriverProfileProps {

View File

@@ -5,7 +5,7 @@ import { Globe, Trophy, UserPlus, Check } from 'lucide-react';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Button } from '@/ui/Button';
import { RatingBadge } from '@/ui/RatingBadge';
import { RatingBadge } from '@/components/drivers/RatingBadge';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Image } from '@/ui/Image';

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { Stack } from './Stack';
import { Box } from './Box';
import { Text } from './Text';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
interface DriverStatsProps {
rating: number;

View File

@@ -1,12 +1,12 @@
import React from 'react';
import { Box } from './Box';
import { Image } from './Image';
import { Link } from './Link';
import { PlaceholderImage } from './PlaceholderImage';
import { Stack } from './Stack';
import { Text } from './Text';
import { Box } from '@/ui/Box';
import { Image } from '@/ui/Image';
import { Link } from '@/ui/Link';
import { PlaceholderImage } from '@/ui/PlaceholderImage';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
interface DriverSummaryPillProps {
name: string;

View File

@@ -1,8 +1,8 @@
import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import { DriverRatingPill } from '@/ui/DriverRatingPill';
import { DriverSummaryPill as UiDriverSummaryPill } from '@/ui/DriverSummaryPill';
import { DriverRatingPill } from '@/components/drivers/DriverRatingPill';
import { DriverSummaryPill as UiDriverSummaryPill } from '@/components/drivers/DriverSummaryPill';
export interface DriverSummaryPillProps {
driver: DriverViewModel;

View File

@@ -1,7 +1,7 @@
'use client';
import React from 'react';
import { RatingBadge } from '@/ui/RatingBadge';
import { RatingBadge } from '@/components/drivers/RatingBadge';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';

View File

@@ -6,7 +6,7 @@ import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Image } from '@/ui/Image';
import { MedalBadge } from '@/ui/MedalBadge';
import { MedalBadge } from '@/components/leaderboards/MedalBadge';
import { MiniStat } from '@/ui/MiniStat';
import { Text } from '@/ui/Text';
import { Flag, Shield, Star, TrendingUp } from 'lucide-react';

View File

@@ -5,7 +5,7 @@ import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { CountryFlag } from '@/ui/CountryFlag';
import { DriverRatingPill } from '@/ui/DriverRatingPill';
import { DriverRatingPill } from '@/components/drivers/DriverRatingPill';
import { Heading } from '@/ui/Heading';
import { Image } from '@/ui/Image';
import { PlaceholderImage } from '@/ui/PlaceholderImage';

View File

@@ -8,7 +8,7 @@ import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/Stack';
import { StatCard } from '@/ui/StatCard';
import { RankBadge } from '@/ui/RankBadge';
import { RankBadge } from '@/components/leaderboards/RankBadge';
interface ProfileStatsProps {
driverId?: string;

View File

@@ -3,8 +3,8 @@
import { Box } from '@/ui/Box';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { RatingComponent } from '@/ui/RatingComponent';
import { RatingHistoryItem } from '@/ui/RatingHistoryItem';
import { RatingComponent } from '@/components/drivers/RatingComponent';
import { RatingHistoryItem } from '@/components/drivers/RatingHistoryItem';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';

View File

@@ -1,9 +1,9 @@
import { Box } from './Box';
import { ProgressBar } from './ProgressBar';
import { Stack } from './Stack';
import { Text } from './Text';
import { Box } from '@/ui/Box';
import { ProgressBar } from '@/ui/ProgressBar';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
interface RatingComponentProps {
label: string;

View File

@@ -1,7 +1,7 @@
import { Box } from './Box';
import { Text } from './Text';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
interface RatingHistoryItemProps {
date: string;

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { LucideIcon } from 'lucide-react';
import { Button } from './Button';
import { Stack } from './Stack';
import { Text } from './Text';
import { Icon } from './Icon';
import { Button } from '@/ui/Button';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
interface SkillLevelButtonProps {
label: string;

View File

@@ -1,11 +1,11 @@
import React from 'react';
import { LucideIcon, ChevronRight, UserPlus } from 'lucide-react';
import { Box } from './Box';
import { Stack } from './Stack';
import { Text } from './Text';
import { Heading } from './Heading';
import { Icon } from './Icon';
import { Badge } from './Badge';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Badge } from '@/ui/Badge';
interface SkillLevelHeaderProps {
label: string;

View File

@@ -1,7 +1,7 @@
import { mediaConfig } from '@/lib/config/mediaConfig';
import { ActiveDriverCard } from '@/ui/ActiveDriverCard';
import { ActiveDriverCard } from '@/components/drivers/ActiveDriverCard';
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';

View File

@@ -1,7 +1,7 @@
'use client';
import React from 'react';
import { LeagueCard } from '@/ui/LeagueCard';
import { LeagueCard } from '@/components/leagues/LeagueCard';
import { routes } from '@/lib/routing/RouteConfig';
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';

View File

@@ -1,7 +1,7 @@
'use client';
import React from 'react';
import { UpcomingRaceItem } from '@/ui/UpcomingRaceItem';
import { UpcomingRaceItem } from '@/components/races/UpcomingRaceItem';
import { routes } from '@/lib/routing/RouteConfig';
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';

View File

@@ -1,7 +1,7 @@
'use client';
import React from 'react';
import { TeamCard } from '@/ui/TeamCard';
import { TeamCard } from '@/components/teams/TeamCard';
import { routes } from '@/lib/routing/RouteConfig';
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';

View File

@@ -9,9 +9,9 @@ import { Heading } from '@/ui/Heading';
import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { LeagueCard } from '@/ui/LeagueCard';
import { TeamCard } from '@/ui/TeamCard';
import { UpcomingRaceItem } from '@/ui/UpcomingRaceItem';
import { LeagueCard } from '@/components/leagues/LeagueCard';
import { TeamCard } from '@/components/teams/TeamCard';
import { UpcomingRaceItem } from '@/components/races/UpcomingRaceItem';
import { HomeViewData } from '@/templates/HomeTemplate';
interface DiscoverySectionProps {

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { NavLink } from './NavLink';
import { Stack } from '@/ui/Stack';
import { Home, Trophy, Layout, Users, Calendar, Settings } from 'lucide-react';
import { routes } from '@/lib/routing/RouteConfig';
interface AuthedNavProps {
pathname: string;
direction?: 'row' | 'col';
}
/**
* AuthedNav displays navigation items for authenticated users.
*/
export function AuthedNav({ pathname, direction = 'col' }: AuthedNavProps) {
const items = [
{ label: 'Dashboard', href: routes.protected.dashboard, icon: Home },
{ label: 'Leagues', href: routes.public.leagues, icon: Trophy },
{ label: 'Leaderboards', href: routes.public.leaderboards, icon: Layout },
{ label: 'Teams', href: routes.public.teams, icon: Users },
{ label: 'Races', href: routes.public.races, icon: Calendar },
{ label: 'Settings', href: routes.protected.profileSettings, icon: Settings },
];
return (
<Stack direction={direction} gap={direction === 'row' ? 4 : 1}>
{items.map((item) => (
<NavLink
key={item.href}
href={item.href}
label={item.label}
icon={item.icon}
isActive={pathname === item.href}
variant={direction === 'row' ? 'top' : 'sidebar'}
/>
))}
</Stack>
);
}

View File

@@ -0,0 +1,41 @@
import React from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { Box } from '@/ui/Box';
interface BrandMarkProps {
href?: string;
priority?: boolean;
}
/**
* BrandMark provides the consistent logo/wordmark for the application.
* Aligned with "Precision Racing Minimal" theme.
*/
export function BrandMark({ href = '/', priority = false }: BrandMarkProps) {
return (
<Box as={Link} href={href} display="inline-flex" alignItems="center" group>
<Box position="relative">
<Box h={{ base: '24px', md: '28px' }} w="auto" transition opacity={1} groupHoverOpacity={0.8}>
<Image
src="/images/logos/wordmark-rectangle-dark.svg"
alt="GridPilot"
width={160}
height={30}
priority={priority}
/>
</Box>
<Box
position="absolute"
bottom="-4px"
left="0"
w="0"
h="2px"
bg="primary-accent"
transition
groupHoverWidth="full"
/>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { Button } from '@/ui/Button';
import { Stack } from '@/ui/Stack';
import { routes } from '@/lib/routing/RouteConfig';
import { LogIn, UserPlus } from 'lucide-react';
interface HeaderActionsProps {
isAuthenticated: boolean;
}
/**
* HeaderActions provides the primary actions in the header (Login, Signup, Profile).
*/
export function HeaderActions({ isAuthenticated }: HeaderActionsProps) {
if (isAuthenticated) {
return (
<Stack direction="row" gap={3}>
<Button as="a" href={routes.protected.profile} variant="secondary" size="sm">
Profile
</Button>
</Stack>
);
}
return (
<Stack direction="row" gap={3}>
<Button
as="a"
href={routes.auth.login}
variant="ghost"
size="sm"
icon={<LogIn size={16} />}
data-testid="public-nav-login"
>
Login
</Button>
<Button
as="a"
href={routes.auth.signup}
variant="primary"
size="sm"
icon={<UserPlus size={16} />}
data-testid="public-nav-signup"
>
Sign Up
</Button>
</Stack>
);
}

View File

@@ -0,0 +1,66 @@
import React from 'react';
import Link from 'next/link';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { LucideIcon } from 'lucide-react';
interface NavLinkProps {
href: string;
label: string;
icon?: LucideIcon;
isActive?: boolean;
variant?: 'sidebar' | 'top';
}
/**
* NavLink provides a consistent link component for navigation.
* Supports both sidebar and top navigation variants.
*/
export function NavLink({ href, label, icon: Icon, isActive, variant = 'sidebar' }: NavLinkProps) {
if (variant === 'top') {
return (
<Box
as={Link}
href={href}
display="flex"
alignItems="center"
gap={2}
px={3}
py={2}
transition
color={isActive ? 'primary-accent' : 'text-gray-400'}
hoverTextColor="white"
>
{Icon && <Icon size={18} />}
<Text size="sm" weight={isActive ? 'bold' : 'medium'}>
{label}
</Text>
</Box>
);
}
return (
<Box
as={Link}
href={href}
display="flex"
alignItems="center"
gap={3}
px={3}
py={2}
rounded="md"
transition
bg={isActive ? 'primary-accent/10' : 'transparent'}
color={isActive ? 'primary-accent' : 'text-gray-400'}
hoverBg={isActive ? 'primary-accent/10' : 'white/5'}
hoverTextColor={isActive ? 'primary-accent' : 'white'}
group
>
{Icon && <Icon size={20} color={isActive ? '#198CFF' : '#6B7280'} />}
<Text weight="medium">{label}</Text>
{isActive && (
<Box ml="auto" w="4px" h="16px" bg="primary-accent" rounded="full" shadow="[0_0_8px_rgba(25,140,255,0.5)]" />
)}
</Box>
);
}

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { NavLink } from './NavLink';
import { Stack } from '@/ui/Stack';
import { Home, Trophy, Layout, Users, Calendar } from 'lucide-react';
import { routes } from '@/lib/routing/RouteConfig';
interface PublicNavProps {
pathname: string;
direction?: 'row' | 'col';
}
/**
* PublicNav displays navigation items for unauthenticated users.
*/
export function PublicNav({ pathname, direction = 'col' }: PublicNavProps) {
const items = [
{ label: 'Home', href: routes.public.home, icon: Home },
{ label: 'Leagues', href: routes.public.leagues, icon: Trophy },
{ label: 'Leaderboards', href: routes.public.leaderboards, icon: Layout },
{ label: 'Teams', href: routes.public.teams, icon: Users },
{ label: 'Races', href: routes.public.races, icon: Calendar },
];
return (
<Stack direction={direction} gap={direction === 'row' ? 4 : 1}>
{items.map((item) => (
<NavLink
key={item.href}
href={item.href}
label={item.label}
icon={item.icon}
isActive={pathname === item.href}
variant={direction === 'row' ? 'top' : 'sidebar'}
/>
))}
</Stack>
);
}

View File

@@ -1,16 +1,12 @@
import React from 'react';
import { Trophy, ChevronRight } from 'lucide-react';
import { Button } from '@/ui/Button';
import { Trophy } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { Image } from '@/ui/Image';
import { LeaderboardList } from '@/ui/LeaderboardList';
import { LeaderboardPreviewShell } from '@/ui/LeaderboardPreviewShell';
import { RankBadge } from '@/components/leaderboards/RankBadge';
import { SkillLevelDisplay } from '@/lib/display-objects/SkillLevelDisplay';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { RankMedal } from './RankMedal';
import { LeaderboardTableShell } from './LeaderboardTableShell';
interface DriverLeaderboardPreviewProps {
drivers: {
@@ -32,54 +28,18 @@ export function DriverLeaderboardPreview({ drivers, onDriverClick, onNavigateToD
const top10 = drivers; // Already sliced in builder
return (
<LeaderboardTableShell>
<Box
display="flex"
alignItems="center"
justifyContent="between"
px={5}
py={4}
borderBottom
borderColor="border-charcoal-outline/50"
bg="bg-deep-charcoal/40"
>
<Box display="flex" alignItems="center" gap={3}>
<Box
display="flex"
h="10"
w="10"
alignItems="center"
justifyContent="center"
rounded="lg"
bg="bg-gradient-to-br from-primary-blue/15 to-primary-blue/5"
border
borderColor="border-primary-blue/20"
>
<Icon icon={Trophy} size={5} color="text-primary-blue" />
</Box>
<Box>
<Heading level={3} fontSize="lg" weight="bold" color="text-white" letterSpacing="tight">Driver Rankings</Heading>
<Text size="xs" color="text-gray-500" block uppercase letterSpacing="wider" weight="bold">Top Performers</Text>
</Box>
</Box>
<Button
variant="secondary"
onClick={onNavigateToDrivers}
size="sm"
hoverBg="bg-primary-blue/10"
transition
>
<Stack direction="row" align="center" gap={2}>
<Text size="sm" weight="medium">View All</Text>
<Icon icon={ChevronRight} size={4} />
</Stack>
</Button>
</Box>
<Stack gap={0}>
<LeaderboardPreviewShell
title="Driver Rankings"
subtitle="Top Performers"
onViewFull={onNavigateToDrivers}
icon={Trophy}
iconColor="var(--primary-blue)"
iconBgGradient="linear-gradient(to bottom right, rgba(25, 140, 255, 0.2), rgba(25, 140, 255, 0.1))"
viewFullLabel="View All"
>
<LeaderboardList>
{top10.map((driver, index) => {
const position = index + 1;
const isLast = index === top10.length - 1;
return (
<Box
@@ -97,11 +57,9 @@ export function DriverLeaderboardPreview({ drivers, onDriverClick, onNavigateToD
transition
hoverBg="bg-white/[0.02]"
group
borderBottom={!isLast}
borderColor="border-charcoal-outline/30"
>
<Box w="8" display="flex" justifyContent="center">
<RankMedal rank={position} size="sm" />
<RankBadge rank={position} />
</Box>
<Box
@@ -111,7 +69,6 @@ export function DriverLeaderboardPreview({ drivers, onDriverClick, onNavigateToD
rounded="full"
overflow="hidden"
border
borderWidth="1px"
borderColor="border-charcoal-outline"
groupHoverBorderColor="primary-blue/50"
transition
@@ -152,7 +109,7 @@ export function DriverLeaderboardPreview({ drivers, onDriverClick, onNavigateToD
</Box>
);
})}
</Stack>
</LeaderboardTableShell>
</LeaderboardList>
</LeaderboardPreviewShell>
);
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/ui/Table';
import { RankingRow } from './RankingRow';
import { LeaderboardTableShell } from './LeaderboardTableShell';
import { LeaderboardTableShell } from '@/ui/LeaderboardTableShell';
interface LeaderboardDriver {
id: string;
@@ -22,28 +22,23 @@ interface LeaderboardTableProps {
}
export function LeaderboardTable({ drivers, onDriverClick }: LeaderboardTableProps) {
const columns = [
{ key: 'rank', label: 'Rank', width: '8rem' },
{ key: 'driver', label: 'Driver' },
{ key: 'races', label: 'Races', align: 'center' as const },
{ key: 'rating', label: 'Rating', align: 'center' as const },
{ key: 'wins', label: 'Wins', align: 'center' as const },
];
return (
<LeaderboardTableShell isEmpty={drivers.length === 0} emptyMessage="No drivers found">
<Table>
<TableHead>
<TableRow>
<TableHeader w="32">Rank</TableHeader>
<TableHeader>Driver</TableHeader>
<TableHeader textAlign="center">Races</TableHeader>
<TableHeader textAlign="center">Rating</TableHeader>
<TableHeader textAlign="center">Wins</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{drivers.map((driver) => (
<RankingRow
key={driver.id}
{...driver}
onClick={() => onDriverClick?.(driver.id)}
/>
))}
</TableBody>
</Table>
<LeaderboardTableShell columns={columns}>
{drivers.map((driver) => (
<RankingRow
key={driver.id}
{...driver}
onClick={() => onDriverClick?.(driver.id)}
/>
))}
</LeaderboardTableShell>
);
}

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { Crown } from 'lucide-react';
import { Box } from './Box';
import { Icon } from './Icon';
import { Text } from './Text';
import { Box } from '@/ui/Box';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
interface MedalBadgeProps {
position: number;

View File

@@ -1,7 +1,7 @@
import { Box } from './Box';
import { Text } from './Text';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
interface RankBadgeProps {
rank: number;

View File

@@ -1,15 +1,12 @@
import React from 'react';
import { Users, ChevronRight } from 'lucide-react';
import { Button } from '@/ui/Button';
import { Users } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { Image } from '@/ui/Image';
import { LeaderboardList } from '@/ui/LeaderboardList';
import { LeaderboardPreviewShell } from '@/ui/LeaderboardPreviewShell';
import { RankBadge } from '@/components/leaderboards/RankBadge';
import { getMediaUrl } from '@/lib/utilities/media';
import { RankMedal } from './RankMedal';
import { LeaderboardTableShell } from './LeaderboardTableShell';
import { Icon } from '@/ui/Icon';
interface TeamLeaderboardPreviewProps {
teams: {
@@ -30,54 +27,18 @@ export function TeamLeaderboardPreview({ teams, onTeamClick, onNavigateToTeams }
const top5 = teams;
return (
<LeaderboardTableShell>
<Box
display="flex"
alignItems="center"
justifyContent="between"
px={5}
py={4}
borderBottom
borderColor="border-charcoal-outline/50"
bg="bg-deep-charcoal/40"
>
<Box display="flex" alignItems="center" gap={3}>
<Box
display="flex"
h="10"
w="10"
alignItems="center"
justifyContent="center"
rounded="lg"
bg="bg-gradient-to-br from-purple-500/15 to-purple-500/5"
border
borderColor="border-purple-500/20"
>
<Icon icon={Users} size={5} color="text-purple-400" />
</Box>
<Box>
<Heading level={3} fontSize="lg" weight="bold" color="text-white" letterSpacing="tight">Team Rankings</Heading>
<Text size="xs" color="text-gray-500" block uppercase letterSpacing="wider" weight="bold">Top Performing Teams</Text>
</Box>
</Box>
<Button
variant="secondary"
onClick={onNavigateToTeams}
size="sm"
hoverBg="bg-purple-500/10"
transition
>
<Stack direction="row" align="center" gap={2}>
<Text size="sm" weight="medium">View All</Text>
<Icon icon={ChevronRight} size={4} />
</Stack>
</Button>
</Box>
<Stack gap={0}>
{top5.map((team, index) => {
<LeaderboardPreviewShell
title="Team Rankings"
subtitle="Top Performing Teams"
onViewFull={onNavigateToTeams}
icon={Users}
iconColor="var(--neon-purple)"
iconBgGradient="linear-gradient(to bottom right, rgba(168, 85, 247, 0.2), rgba(168, 85, 247, 0.1))"
viewFullLabel="View All"
>
<LeaderboardList>
{top5.map((team) => {
const position = team.position;
const isLast = index === top5.length - 1;
return (
<Box
@@ -95,11 +56,9 @@ export function TeamLeaderboardPreview({ teams, onTeamClick, onNavigateToTeams }
transition
hoverBg="bg-white/[0.02]"
group
borderBottom={!isLast}
borderColor="border-charcoal-outline/30"
>
<Box w="8" display="flex" justifyContent="center">
<RankMedal rank={position} size="sm" />
<RankBadge rank={position} />
</Box>
<Box
@@ -166,7 +125,7 @@ export function TeamLeaderboardPreview({ teams, onTeamClick, onNavigateToTeams }
</Box>
);
})}
</Stack>
</LeaderboardTableShell>
</LeaderboardList>
</LeaderboardPreviewShell>
);
}

View File

@@ -1,15 +1,15 @@
import { CheckCircle2, Clock, Star } from 'lucide-react';
import { Badge } from './Badge';
import { Box } from './Box';
import { Button } from './Button';
import { Card } from './Card';
import { Heading } from './Heading';
import { Icon } from './Icon';
import { Link } from './Link';
import { Stack } from './Stack';
import { Text } from './Text';
import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
interface AvailableLeague {
id: string;

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { Box } from './Box';
import { Text } from './Text';
import { Stack } from './Stack';
import { Button } from './Button';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Button } from '@/ui/Button';
interface JoinRequestItemProps {
driverId: string;

View File

@@ -1,5 +1,5 @@
import React, { ReactNode } from 'react';
import { Stack } from './Stack';
import { Stack } from '@/ui/Stack';
interface JoinRequestListProps {
children: ReactNode;

View File

@@ -1,11 +1,11 @@
import React from 'react';
import { Box } from './Box';
import { Stack } from './Stack';
import { Text } from './Text';
import { Heading } from './Heading';
import { Button } from './Button';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Button } from '@/ui/Button';
import { Check, X, Clock } from 'lucide-react';
import { Icon } from './Icon';
import { Icon } from '@/ui/Icon';
interface JoinRequestsPanelProps {
requests: Array<{

View File

@@ -1,23 +1,32 @@
'use client';
import React from 'react';
import { ReactNode } from 'react';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Image } from '@/ui/Image';
import { Trophy, Users, Calendar, ChevronRight } from 'lucide-react';
import { PlaceholderImage } from '@/ui/PlaceholderImage';
import { Calendar as LucideCalendar, ChevronRight as LucideChevronRight } from 'lucide-react';
interface LeagueCardProps {
id: string;
name: string;
description?: string;
coverUrl: string;
logoUrl?: string;
gameName?: string;
memberCount: number;
maxMembers?: number;
nextRaceDate?: string;
championshipType: 'driver' | 'team' | 'nations' | 'trophy';
badges?: ReactNode;
championshipBadge?: ReactNode;
slotLabel: string;
usedSlots: number;
maxSlots: number | string;
fillPercentage: number;
hasOpenSlots: boolean;
openSlotsCount: number;
isTeamLeague?: boolean;
usedDriverSlots?: number;
maxDrivers?: number | string;
timingSummary?: string;
onClick?: () => void;
}
@@ -26,154 +35,154 @@ export function LeagueCard({
description,
coverUrl,
logoUrl,
gameName,
memberCount,
maxMembers,
nextRaceDate,
championshipType,
badges,
championshipBadge,
slotLabel,
usedSlots,
maxSlots,
fillPercentage,
hasOpenSlots,
openSlotsCount,
isTeamLeague: _isTeamLeague,
usedDriverSlots: _usedDriverSlots,
maxDrivers: _maxDrivers,
timingSummary,
onClick,
}: LeagueCardProps) {
const fillPercentage = maxMembers ? (memberCount / maxMembers) * 100 : 0;
return (
<Box
as="article"
onClick={onClick}
<Box
position="relative"
display="flex"
flexDirection="col"
overflow="hidden"
border
borderColor="zinc-800"
bg="zinc-900/50"
hoverBorderColor="blue-500/30"
hoverBg="zinc-900"
transition
cursor="pointer"
group
cursor={onClick ? 'pointer' : 'default'}
h="full"
onClick={onClick}
className="group"
>
{/* Cover Image */}
<Box position="relative" h="32" overflow="hidden">
<Box fullWidth fullHeight opacity={0.6}>
{/* Card Container */}
<Box
position="relative"
h="full"
rounded="none"
bg="panel-gray/40"
border
borderColor="border-gray/50"
overflow="hidden"
transition
className="hover:border-primary-accent/30 hover:bg-panel-gray/60 transition-all duration-300"
>
{/* Cover Image */}
<Box position="relative" h="32" overflow="hidden">
<Image
src={coverUrl}
alt={`${name} cover`}
fullWidth
fullHeight
objectFit="cover"
// eslint-disable-next-line gridpilot-rules/component-classification
className="transition-transform duration-500 group-hover:scale-105"
className="transition-transform duration-500 group-hover:scale-105 opacity-60"
/>
</Box>
<Box position="absolute" inset="0" bg="linear-gradient(to top, #09090b, transparent)" />
{/* Game Badge */}
{gameName && (
<Box
position="absolute"
top="3"
left="3"
px={2}
py={1}
bg="zinc-900/80"
border
borderColor="white/10"
blur="sm"
>
<Text weight="bold" color="text-zinc-300" uppercase letterSpacing="0.05em" fontSize="10px">
{gameName}
</Text>
{/* Gradient Overlay */}
<Box position="absolute" inset="0" bg="linear-gradient(to top, #0C0D0F, transparent)" />
{/* Badges - Top Left */}
<Box position="absolute" top="3" left="3" display="flex" alignItems="center" gap={2}>
{badges}
</Box>
)}
{/* Championship Icon */}
<Box
position="absolute"
top="3"
right="3"
p={1.5}
bg="zinc-900/80"
color="text-zinc-400"
border
borderColor="white/10"
blur="sm"
>
{championshipType === 'driver' && <Trophy size={14} />}
{championshipType === 'team' && <Users size={14} />}
</Box>
</Box>
{/* Championship Type Badge - Top Right */}
<Box position="absolute" top="3" right="3">
{championshipBadge}
</Box>
{/* Content */}
<Box position="relative" display="flex" flexDirection="col" flexGrow={1} p={4} pt={6}>
{/* Logo */}
<Box
position="absolute"
top="-6"
left="4"
w="12"
h="12"
border
borderColor="zinc-800"
bg="zinc-950"
shadow="xl"
overflow="hidden"
>
{logoUrl ? (
<Image src={logoUrl} alt={`${name} logo`} fullWidth fullHeight objectFit="cover" />
) : (
<Box fullWidth fullHeight display="flex" alignItems="center" justifyContent="center" bg="zinc-900" color="text-zinc-700">
<Trophy size={20} />
{/* Logo */}
<Box position="absolute" left="4" bottom="-6" zIndex={10}>
<Box w="12" h="12" rounded="none" overflow="hidden" border borderColor="border-gray/50" bg="graphite-black" shadow="xl">
{logoUrl ? (
<Image
src={logoUrl}
alt={`${name} logo`}
width={48}
height={48}
fullWidth
fullHeight
objectFit="cover"
/>
) : (
<PlaceholderImage size={48} />
)}
</Box>
)}
</Box>
</Box>
<Box display="flex" flexDirection="col" gap={1} mb={4}>
<Heading level={3} fontSize="lg" weight="bold" color="text-white"
// eslint-disable-next-line gridpilot-rules/component-classification
className="group-hover:text-blue-400 transition-colors truncate"
>
{name}
</Heading>
<Text size="xs" color="text-zinc-500" lineClamp={2} leading="relaxed" h="8">
{/* Content */}
<Box pt={8} px={4} pb={4} display="flex" flexDirection="col" fullHeight>
{/* Title & Description */}
<Stack direction="row" align="center" gap={2} mb={1}>
<Box w="1" h="4" bg="primary-accent" />
<Heading level={3} fontSize="lg" weight="bold" className="line-clamp-1 group-hover:text-primary-accent transition-colors tracking-tight">
{name}
</Heading>
</Stack>
<Text size="xs" color="text-gray-500" lineClamp={2} mb={4} style={{ height: '2.5rem' }} block leading="relaxed">
{description || 'No description available'}
</Text>
</Box>
{/* Stats */}
<Box display="flex" flexDirection="col" gap={3} mt="auto">
<Box display="flex" flexDirection="col" gap={1.5}>
<Box display="flex" justifyContent="between">
<Text weight="bold" color="text-zinc-500" uppercase letterSpacing="widest" fontSize="10px">Drivers</Text>
<Text color="text-zinc-400" font="mono" fontSize="10px">{memberCount}/{maxMembers || '∞'}</Text>
</Box>
<Box h="1" bg="zinc-800" overflow="hidden">
<Box
h="full"
transition
bg={fillPercentage > 90 ? 'bg-amber-500' : 'bg-blue-500'}
w={`${Math.min(fillPercentage, 100)}%`}
/>
{/* Stats Row */}
<Box display="flex" alignItems="center" gap={3} mb={4}>
{/* Primary Slots (Drivers/Teams/Nations) */}
<Box flexGrow={1}>
<Box display="flex" alignItems="center" justifyContent="between" mb={1.5}>
<Text size="xs" color="text-gray-500" weight="bold" className="uppercase tracking-widest">{slotLabel}</Text>
<Text size="xs" color="text-gray-400" font="mono">
{usedSlots}/{maxSlots || '∞'}
</Text>
</Box>
<Box h="1" rounded="none" bg="border-gray/30" overflow="hidden">
<Box
h="full"
rounded="none"
transition
bg={
fillPercentage >= 90
? 'warning-amber'
: fillPercentage >= 70
? 'primary-accent'
: 'success-green'
}
style={{ width: `${Math.min(fillPercentage, 100)}%` }}
/>
</Box>
</Box>
{/* Open Slots Badge */}
{hasOpenSlots && (
<Box display="flex" alignItems="center" gap={1.5} px={2} py={1} rounded="none" bg="primary-accent/5" border borderColor="primary-accent/20">
<Box w="1.5" h="1.5" rounded="full" bg="primary-accent" className="animate-pulse" />
<Text size="xs" color="text-primary-accent" weight="bold" className="uppercase tracking-tighter">
{openSlotsCount} OPEN
</Text>
</Box>
)}
</Box>
<Box display="flex" alignItems="center" justifyContent="between" pt={3} borderTop borderColor="zinc-800/50">
<Box display="flex" alignItems="center" gap={2} color="text-zinc-500">
<Calendar size={12} />
<Text weight="bold" uppercase font="mono" fontSize="10px">
{nextRaceDate || 'TBD'}
</Text>
{/* Spacer to push footer to bottom */}
<Box flexGrow={1} />
{/* Footer Info */}
<Box display="flex" alignItems="center" justifyContent="between" pt={3} borderTop borderColor="border-gray/30" mt="auto">
<Box display="flex" alignItems="center" gap={3}>
{timingSummary && (
<Box display="flex" alignItems="center" gap={2}>
<Icon icon={LucideCalendar} size={3} color="text-gray-500" />
<Text size="xs" color="text-gray-500" font="mono">
{timingSummary.split('•')[1]?.trim() || timingSummary}
</Text>
</Box>
)}
</Box>
<Box display="flex" alignItems="center" gap={1} color="text-zinc-500"
// eslint-disable-next-line gridpilot-rules/component-classification
className="group-hover:text-blue-400 transition-colors"
>
<Text weight="bold" uppercase letterSpacing="widest" fontSize="10px">View</Text>
<Box
// eslint-disable-next-line gridpilot-rules/component-classification
className="transition-transform group-hover:translate-x-0.5"
>
<ChevronRight size={12} />
</Box>
{/* View Arrow */}
<Box display="flex" alignItems="center" gap={1} className="group-hover:text-primary-accent transition-colors">
<Text size="xs" color="text-gray-500" weight="bold" className="uppercase tracking-widest">VIEW</Text>
<Icon icon={LucideChevronRight} size={3} color="text-gray-500" className="transition-transform group-hover:translate-x-0.5" />
</Box>
</Box>
</Box>
@@ -181,3 +190,4 @@ export function LeagueCard({
</Box>
);
}

View File

@@ -9,7 +9,7 @@ import {
import type { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
import { getMediaUrl } from '@/lib/utilities/media';
import { Badge } from '@/ui/Badge';
import { LeagueCard as UiLeagueCard } from '@/ui/LeagueCard';
import { LeagueCard as UiLeagueCard } from './LeagueCard';
interface LeagueCardProps {
league: LeagueSummaryViewModel;
@@ -117,8 +117,8 @@ export function LeagueCard({ league, onClick }: LeagueCardProps) {
const gameVariant = getGameVariant(league.scoring?.gameId);
const isNew = isNewLeague(league.createdAt);
const isTeamLeague = league.maxTeams && league.maxTeams > 0;
const categoryLabel = getCategoryLabel(league.category);
const categoryVariant = getCategoryVariant(league.category);
const categoryLabel = getCategoryLabel(league.category || undefined);
const categoryVariant = getCategoryVariant(league.category || undefined);
const usedSlots = isTeamLeague ? (league.usedTeamSlots ?? 0) : (league.usedDriverSlots ?? 0);
const maxSlots = isTeamLeague ? (league.maxTeams ?? 0) : (league.maxDrivers ?? 0);
@@ -135,7 +135,7 @@ export function LeagueCard({ league, onClick }: LeagueCardProps) {
return (
<UiLeagueCard
name={league.name}
description={league.description}
description={league.description || undefined}
coverUrl={coverUrl}
logoUrl={logoUrl || undefined}
slotLabel={slotLabel}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { Box } from './Box';
import { Image } from './Image';
import { ImagePlaceholder } from './ImagePlaceholder';
import { Box } from '@/ui/Box';
import { SafeImage } from '@/components/shared/SafeImage';
import { ImagePlaceholder } from '@/ui/ImagePlaceholder';
export interface LeagueCoverProps {
leagueId?: string;
@@ -20,7 +20,7 @@ export function LeagueCover({
aspectRatio = '21/9',
className = '',
}: LeagueCoverProps) {
const coverSrc = src || (leagueId ? `/media/leagues/${leagueId}/cover` : undefined);
const coverSrc = src || (leagueId ? `/api/media/leagues/${leagueId}/cover` : undefined);
return (
<Box
@@ -31,11 +31,11 @@ export function LeagueCover({
style={{ height, aspectRatio: height ? undefined : aspectRatio }}
>
{coverSrc ? (
<Image
<SafeImage
src={coverSrc}
alt={alt}
className="w-full h-full object-cover"
fallbackSrc="/default-league-cover.png"
fallbackComponent={<ImagePlaceholder aspectRatio={aspectRatio} rounded="none" />}
/>
) : (
<ImagePlaceholder aspectRatio={aspectRatio} rounded="none" />

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { LeagueCover as UiLeagueCover } from '@/ui/LeagueCover';
import { LeagueCover as UiLeagueCover } from '@/components/leagues/LeagueCover';
export interface LeagueCoverProps {
leagueId: string;

View File

@@ -1,89 +1,62 @@
'use client';
import React from 'react';
import { ReactNode } from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Image } from '@/ui/Image';
import { MembershipStatus } from './MembershipStatus';
interface MainSponsorInfo {
interface LeagueHeaderProps {
name: string;
logoUrl?: string;
websiteUrl?: string;
}
export interface LeagueHeaderProps {
leagueId: string;
leagueName: string;
description?: string | null;
ownerId: string;
ownerName: string;
mainSponsor?: MainSponsorInfo | null;
logoUrl: string;
sponsorContent?: ReactNode;
statusContent?: ReactNode;
}
export function LeagueHeader({
leagueId,
leagueName,
name,
description,
mainSponsor,
logoUrl,
sponsorContent,
statusContent,
}: LeagueHeaderProps) {
return (
<Box as="header" mb={8}>
<Stack direction="row" align="center" gap={6}>
<Box
position="relative"
w="20"
h="20"
overflow="hidden"
border
borderColor="white/10"
bg="zinc-900"
shadow="2xl"
>
<Image
src={`/api/media/league-logo/${leagueId}`}
alt={`${leagueName} logo`}
fullWidth
fullHeight
objectFit="cover"
/>
</Box>
<Stack gap={1}>
<Stack direction="row" align="center" gap={4}>
<Heading level={1} fontSize="3xl" weight="bold" color="text-white">
{leagueName}
{mainSponsor && (
<Text ml={3} size="lg" weight="normal" color="text-zinc-500">
by{' '}
{mainSponsor.websiteUrl ? (
<Box
as="a"
href={mainSponsor.websiteUrl}
target="_blank"
rel="noreferrer"
color="text-blue-500"
hoverTextColor="text-blue-400"
transition
>
{mainSponsor.name}
</Box>
) : (
<Text color="text-blue-500">{mainSponsor.name}</Text>
)}
</Text>
)}
</Heading>
<MembershipStatus leagueId={leagueId} />
</Stack>
{description && (
<Text color="text-zinc-400" size="sm" maxWidth="2xl" block leading="relaxed">
{description}
</Text>
)}
<Box mb={8}>
<Box display="flex" alignItems="center" justifyContent="between" mb={6}>
<Stack direction="row" align="center" gap={4}>
<Box h="16" w="16" rounded="xl" overflow="hidden" border style={{ borderColor: 'rgba(38, 38, 38, 0.8)', backgroundColor: '#1a1d23' }} shadow="lg">
<Image
src={logoUrl}
alt={`${name} logo`}
width={64}
height={64}
fullWidth
fullHeight
objectFit="cover"
/>
</Box>
<Box>
<Box display="flex" alignItems="center" gap={3} mb={1}>
<Heading level={1}>
{name}
{sponsorContent && (
<Text color="text-gray-400" weight="normal" size="lg" ml={2}>
by {sponsorContent}
</Text>
)}
</Heading>
{statusContent}
</Box>
{description && (
<Text color="text-gray-400" size="sm" maxWidth="xl" block>
{description}
</Text>
)}
</Box>
</Stack>
</Stack>
</Box>
</Box>
);
}

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { Box } from './Box';
import { Image } from './Image';
import { Box } from '@/ui/Box';
import { SafeImage } from '@/components/shared/SafeImage';
import { Trophy } from 'lucide-react';
import { Icon } from './Icon';
import { Icon } from '@/ui/Icon';
export interface LeagueLogoProps {
leagueId?: string;
@@ -23,7 +23,7 @@ export function LeagueLogo({
border = true,
rounded = 'md',
}: LeagueLogoProps) {
const logoSrc = src || (leagueId ? `/media/leagues/${leagueId}/logo` : undefined);
const logoSrc = src || (leagueId ? `/api/media/leagues/${leagueId}/logo` : undefined);
return (
<Box
@@ -39,11 +39,11 @@ export function LeagueLogo({
style={{ width: size, height: size, flexShrink: 0 }}
>
{logoSrc ? (
<Image
<SafeImage
src={logoSrc}
alt={alt}
className="w-full h-full object-contain p-1"
fallbackSrc="/default-league-logo.png"
fallbackComponent={<Icon icon={Trophy} size={size > 32 ? 5 : 4} color="text-gray-500" />}
/>
) : (
<Icon icon={Trophy} size={size > 32 ? 5 : 4} color="text-gray-500" />

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { LeagueLogo as UiLeagueLogo } from '@/ui/LeagueLogo';
import { LeagueLogo as UiLeagueLogo } from '@/components/leagues/LeagueLogo';
export interface LeagueLogoProps {
leagueId: string;

View File

@@ -1,5 +1,5 @@
import React, { ReactNode } from 'react';
import { Table, TableHead, TableBody, TableRow, TableHeader } from './Table';
import { Table, TableHead, TableBody, TableRow, TableHeader } from '@/ui/Table';
interface LeagueMemberTableProps {
children: ReactNode;

View File

@@ -12,7 +12,7 @@ import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Select } from '@/ui/Select';
import { Button } from '@/ui/Button';
import { LeagueMemberTable } from '@/ui/LeagueMemberTable';
import { LeagueMemberTable } from '@/components/leagues/LeagueMemberTable';
import { LeagueMemberRow } from '@/components/leagues/LeagueMemberRow';
import { MinimalEmptyState } from '@/components/shared/state/EmptyState';

View File

@@ -1,17 +1,17 @@
import { ArrowRight } from 'lucide-react';
import { Box } from './Box';
import { Button } from './Button';
import { Card } from './Card';
import { Grid } from './Grid';
import { Heading } from './Heading';
import { Icon } from './Icon';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Grid } from '@/ui/Grid';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { LeagueLogo } from './LeagueLogo';
import { Link } from './Link';
import { Stack } from './Stack';
import { Surface } from './Surface';
import { Text } from './Text';
import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
interface LeagueSummaryCardProps {
id: string;

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { LeagueSummaryCard as UiLeagueSummaryCard } from '@/ui/LeagueSummaryCard';
import { LeagueSummaryCard as UiLeagueSummaryCard } from './LeagueSummaryCard';
import { routes } from '@/lib/routing/RouteConfig';
interface LeagueSummaryCardProps {

View File

@@ -1,8 +1,8 @@
import React, { ReactNode } from 'react';
import { Box } from './Box';
import { Stack } from './Stack';
import { Text } from './Text';
import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from './Table';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from '@/ui/Table';
interface RosterTableProps {
children: ReactNode;

View File

@@ -2,7 +2,7 @@
import React from 'react';
import { Grid } from '@/ui/Grid';
import { LiveryCard } from '@/ui/LiveryCard';
import { LiveryCard } from '@/components/drivers/LiveryCard';
import { ProfileSection } from './ProfileSection';
import { ProfileLiveryViewData } from '@/lib/view-data/ProfileLiveriesViewData';

View File

@@ -4,7 +4,7 @@ import React from 'react';
import { Stack } from '@/ui/Stack';
import { Card } from '@/ui/Card';
import { Text } from '@/ui/Text';
import { LeagueListItem } from '@/ui/LeagueListItem';
import { LeagueListItem } from '@/components/leagues/LeagueListItem';
import { ProfileSection } from './ProfileSection';
interface League {

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { RaceResultList } from '@/ui/RaceResultList';
import { RaceSummaryItem } from '@/ui/RaceSummaryItem';
import { RaceResultList } from '@/components/races/RaceResultList';
import { RaceSummaryItem } from '@/components/races/RaceSummaryItem';
import { Box } from '@/ui/Box';
type RaceWithResults = {

View File

@@ -1,7 +1,7 @@
import { Box } from '@/ui/Box';
import { LiveRaceItem } from '@/ui/LiveRaceItem';
import { LiveRaceItem } from '@/components/races/LiveRaceItem';
import { Text } from '@/ui/Text';
interface LiveRaceBannerProps {

View File

@@ -1,10 +1,10 @@
import { ChevronRight, PlayCircle } from 'lucide-react';
import { Box } from './Box';
import { Heading } from './Heading';
import { Icon } from './Icon';
import { Text } from './Text';
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
interface LiveRaceItemProps {
track: string;

View File

@@ -2,7 +2,7 @@
import type { RaceViewData } from '@/lib/view-data/RacesViewData';
import { Box } from '@/ui/Box';
import { LiveRaceItem } from '@/ui/LiveRaceItem';
import { LiveRaceItem } from '@/components/races/LiveRaceItem';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';

View File

@@ -1,15 +1,15 @@
import { Calendar, ChevronRight, Clock } from 'lucide-react';
import { Badge } from './Badge';
import { Box } from './Box';
import { Button } from './Button';
import { Heading } from './Heading';
import { Icon } from './Icon';
import { Link } from './Link';
import { Stack } from './Stack';
import { Surface } from './Surface';
import { Text } from './Text';
import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
interface NextRaceCardProps {
track: string;

View File

@@ -1,7 +1,7 @@
import { routes } from '@/lib/routing/RouteConfig';
import { NextRaceCard as UiNextRaceCard } from '@/ui/NextRaceCard';
import { NextRaceCard as UiNextRaceCard } from '@/components/races/NextRaceCard';
interface NextRaceCardProps {
nextRace: {

View File

@@ -1,10 +1,10 @@
import { Badge } from './Badge';
import { Box } from './Box';
import { Stack } from './Stack';
import { Surface } from './Surface';
import { Text } from './Text';
import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
interface PenaltyRowProps {
driverName: string;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { Stack } from './Stack';
import { Button } from './Button';
import { Icon } from './Icon';
import { Stack } from '@/ui/Stack';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Trophy, Scale, LogOut, CheckCircle, XCircle, PlayCircle } from 'lucide-react';
interface RaceActionBarProps {

View File

@@ -1,91 +1,171 @@
'use client';
import React from 'react';
import { Clock, MapPin, Users } from 'lucide-react';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Icon } from '@/ui/Icon';
import { ArrowRight, Car, ChevronRight, LucideIcon, Trophy, Zap } from 'lucide-react';
import { Box } from '@/ui/Box';
import { SessionStatusBadge, type SessionStatus } from './SessionStatusBadge';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
import { routes } from '@/lib/routing/RouteConfig';
interface RaceCardProps {
id: string;
title: string;
leagueName: string;
trackName: string;
track: string;
car: string;
scheduledAt: string;
entrantCount: number;
status: SessionStatus;
onClick: (id: string) => void;
status: string;
leagueName: string;
leagueId?: string;
strengthOfField?: number | null;
onClick?: () => void;
statusConfig: {
border: string;
bg: string;
color: string;
icon: LucideIcon | null;
label: string;
};
}
export function RaceCard({
id,
title,
leagueName,
trackName,
track,
car,
scheduledAt,
entrantCount,
status,
leagueName,
leagueId,
strengthOfField,
onClick,
statusConfig,
}: RaceCardProps) {
const scheduledAtDate = new Date(scheduledAt);
return (
<Box
as="article"
onClick={() => onClick(id)}
<Surface
bg="bg-surface-charcoal"
rounded="xl"
border
borderColor="border-outline-steel"
p={4}
padding={4}
onClick={onClick}
cursor={onClick ? 'pointer' : 'default'}
hoverBorderColor="border-primary-accent"
transition
cursor="pointer"
position="relative"
overflow="hidden"
group
>
{/* Hover Glow */}
<Box
position="absolute"
inset="0"
bg="bg-primary-accent"
bgOpacity={0.05}
opacity={0}
groupHoverOpacity={1}
transition
/>
<Stack gap={4}>
<Stack direction="row" justifyContent="between" alignItems="start">
<Stack gap={1}>
<Text size="xs" color="text-gray-500" weight="bold" uppercase>
{leagueName}
</Text>
<Text size="lg" weight="bold" groupHoverTextColor="text-primary-accent">
{title}
</Text>
</Stack>
<SessionStatusBadge status={status} />
</Stack>
{/* Live indicator */}
{status === 'running' && (
<Box
position="absolute"
top="0"
left="0"
right="0"
h="1"
bg="bg-success-green"
animate="pulse"
/>
)}
<Box display="grid" gridCols={2} gap={4}>
<Stack direction="row" alignItems="center" gap={2}>
<Icon icon={MapPin} size={3} color="#6b7280" />
<Text size="xs" color="text-gray-400">{trackName}</Text>
</Stack>
<Stack direction="row" alignItems="center" gap={2}>
<Icon icon={Clock} size={3} color="#6b7280" />
<Text size="xs" color="text-gray-400">{scheduledAt}</Text>
</Stack>
<Stack direction="row" align="start" gap={4}>
{/* Time Column */}
<Box textAlign="center" flexShrink={0} width="16">
<Text size="lg" weight="bold" color="text-white" block>
{scheduledAtDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</Text>
<Text size="xs" color={statusConfig.color} block>
{status === 'running' ? 'LIVE' : scheduledAtDate.toLocaleDateString()}
</Text>
</Box>
<Stack direction="row" alignItems="center" gap={2} pt={2} borderTop borderColor="border-outline-steel" bgOpacity={0.5}>
<Icon icon={Users} size={3} color="#4ED4E0" />
<Text size="xs" color="text-gray-400">
<Text as="span" color="text-telemetry-aqua" weight="bold">{entrantCount}</Text> ENTRANTS
</Text>
</Stack>
{/* Divider */}
<Box
w="px"
bg="border-outline-steel"
alignSelf="stretch"
/>
{/* Main Content */}
<Box flex={1} minWidth="0">
<Stack direction="row" align="start" justify="between" gap={4}>
<Box minWidth="0">
<Heading
level={3}
truncate
groupHoverTextColor="text-primary-accent"
transition
>
{track}
</Heading>
<Stack direction="row" align="center" gap={3} mt={1}>
<Stack direction="row" align="center" gap={1}>
<Icon icon={Car} size={3.5} color="var(--text-gray-400)" />
<Text size="sm" color="text-gray-400">
{car}
</Text>
</Stack>
{strengthOfField && (
<Stack direction="row" align="center" gap={1}>
<Icon icon={Zap} size={3.5} color="var(--warning-amber)" />
<Text size="sm" color="text-gray-400">
SOF {strengthOfField}
</Text>
</Stack>
)}
</Stack>
</Box>
{/* Status Badge */}
<Box
display="flex"
alignItems="center"
gap={1.5}
px={2.5}
py={1}
rounded="full"
border
borderColor="border-outline-steel"
bg="bg-base-black"
bgOpacity={0.5}
>
{statusConfig.icon && (
<Icon icon={statusConfig.icon} size={3.5} color={statusConfig.color} />
)}
<Text size="xs" weight="medium" color={statusConfig.color}>
{statusConfig.label}
</Text>
</Box>
</Stack>
{/* League Link */}
<Box mt={3} pt={3} borderTop borderColor="border-outline-steel" borderOpacity={0.3}>
<Link
href={routes.league.detail(leagueId ?? '')}
onClick={(e) => e.stopPropagation()}
>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Trophy} size={3.5} color="var(--primary-accent)" />
<Text size="sm" color="text-primary-accent">
{leagueName}
</Text>
<Icon icon={ArrowRight} size={3} color="var(--primary-accent)" />
</Stack>
</Link>
</Box>
</Box>
{/* Arrow */}
<Icon
icon={ChevronRight}
size={5}
color="var(--text-gray-500)"
groupHoverTextColor="text-primary-accent"
transition
flexShrink={0}
/>
</Stack>
</Box>
</Surface>
);
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { LucideIcon } from 'lucide-react';
import { raceStatusConfig } from '@/lib/utilities/raceStatus';
import { RaceCard as UiRaceCard } from '@/ui/RaceCard';
import { RaceCard as UiRaceCard } from './RaceCard';
interface RaceCardProps {
race: {

View File

@@ -1,4 +1,4 @@
'use client';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
@@ -49,7 +49,7 @@ export function RaceFilterModal({
isOpen={isOpen}
onOpenChange={(open) => !open && onClose()}
title="Filters"
icon={<Icon icon={Filter} size={5} color="text-primary-blue" />}
icon={<Icon icon={Filter} size={5} color="text-primary-accent" />}
>
<Stack gap={4}>
{/* Search */}
@@ -76,7 +76,7 @@ export function RaceFilterModal({
size="sm"
onClick={() => setTimeFilter(filter)}
>
{filter === 'live' && <Box as="span" width="2" height="2" bg="bg-performance-green" rounded="full" mr={1.5} animate="pulse" />}
{filter === 'live' && <Box as="span" width="2" height="2" bg="bg-success-green" rounded="full" mr={1.5} animate="pulse" />}
{filter.charAt(0).toUpperCase() + filter.slice(1)}
</Button>
))}

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { Box } from './Box';
import { Text } from './Text';
import { Stack } from './Stack';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { RaceStatusBadge } from './RaceStatusBadge';
import { Icon } from './Icon';
import { Icon } from '@/ui/Icon';
import { Calendar, MapPin, Car } from 'lucide-react';
interface RaceHeaderPanelProps {

Some files were not shown because too many files have changed in this diff Show More