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

View File

@@ -1,6 +1,7 @@
import { notFound, redirect } from 'next/navigation'; import { notFound, redirect } from 'next/navigation';
import { DashboardPageQuery } from '@/lib/page-queries/DashboardPageQuery'; import { DashboardPageQuery } from '@/lib/page-queries/DashboardPageQuery';
import { DashboardTemplate } from '@/templates/DashboardTemplate'; import { DashboardTemplate } from '@/templates/DashboardTemplate';
import { logger } from '@/lib/infrastructure/logging/logger';
export default async function DashboardPage() { export default async function DashboardPage() {
const result = await DashboardPageQuery.execute(); const result = await DashboardPageQuery.execute();
@@ -15,7 +16,7 @@ export default async function DashboardPage() {
redirect('/'); redirect('/');
} else { } else {
// serverError, networkError, unknown, validationError, unauthorized // serverError, networkError, unknown, validationError, unauthorized
console.error('Dashboard error:', error); logger.error('Dashboard error', undefined, { errorType: error });
notFound(); 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 './globals.css';
import { AppWrapper } from '@/components/AppWrapper'; import { AppWrapper } from '@/components/AppWrapper';
import { RootAppShellTemplate } from '@/templates/layout/RootAppShellTemplate'; import { RootAppShellTemplate } from '@/templates/layout/RootAppShellTemplate';
import { getWebsiteServerEnv } from '@/lib/config/env';
import { logger } from '@/lib/infrastructure/logging/logger';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -46,7 +48,8 @@ export default async function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
// Initialize debug tools in development // Initialize debug tools in development
if (process.env.NODE_ENV === 'development') { const env = getWebsiteServerEnv();
if (env.NODE_ENV === 'development') {
try { try {
initializeGlobalErrorHandling({ initializeGlobalErrorHandling({
showDevOverlay: true, showDevOverlay: true,
@@ -59,7 +62,7 @@ export default async function RootLayout({
logResponses: true, logResponses: true,
}); });
} catch (error) { } 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 { DriverRankingsPageQuery } from '@/lib/page-queries/DriverRankingsPageQuery';
import { DriverRankingsPageClient } from './DriverRankingsPageClient'; import { DriverRankingsPageClient } from './DriverRankingsPageClient';
import { routes } from '@/lib/routing/RouteConfig'; import { routes } from '@/lib/routing/RouteConfig';
import { logger } from '@/lib/infrastructure/logging/logger';
export default async function DriverLeaderboardPage() { export default async function DriverLeaderboardPage() {
const result = await DriverRankingsPageQuery.execute(); const result = await DriverRankingsPageQuery.execute();
@@ -16,7 +17,7 @@ export default async function DriverLeaderboardPage() {
redirect(routes.public.home); redirect(routes.public.home);
} else { } else {
// serverError, networkError, unknown, validationError, unauthorized // serverError, networkError, unknown, validationError, unauthorized
console.error('Driver rankings error:', error); logger.error('Driver rankings error:', undefined, { error });
notFound(); notFound();
} }
} }

View File

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

View File

@@ -1,7 +1,8 @@
'use client'; 'use client';
import React, { useState } from 'react'; 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 { routes } from '@/lib/routing/RouteConfig';
import type { LeaguesViewData } from '@/lib/view-data/LeaguesViewData'; import type { LeaguesViewData } from '@/lib/view-data/LeaguesViewData';
import { Box } from '@/ui/Box'; import { Box } from '@/ui/Box';
@@ -26,7 +27,6 @@ import {
type LucideIcon, type LucideIcon,
} from 'lucide-react'; } from 'lucide-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { getMediaUrl } from '@/lib/utilities/media';
// ============================================================================ // ============================================================================
// TYPES // TYPES
@@ -252,15 +252,7 @@ export function LeaguesPageClient({ viewData }: LeaguesTemplateProps) {
{filteredLeagues.map((league) => ( {filteredLeagues.map((league) => (
<LeagueCard <LeagueCard
key={league.id} key={league.id}
id={league.id} league={league as unknown as LeagueSummaryViewModel}
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'}
onClick={() => router.push(routes.league.detail(league.id))} onClick={() => router.push(routes.league.detail(league.id))}
/> />
))} ))}

View File

@@ -2,7 +2,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { LeagueRulebookTemplate } from '@/templates/LeagueRulebookTemplate'; 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'; import type { LeagueRulebookViewData } from '@/lib/view-data/LeagueRulebookViewData';
interface LeagueRulebookPageClientProps { interface LeagueRulebookPageClientProps {

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo } from 'react';
import { SponsorLeaguesTemplate, type SortOption, type TierFilter, type AvailabilityFilter } from '@/templates/SponsorLeaguesTemplate'; 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 [searchQuery, setSearchQuery] = useState('');
const [tierFilter] = useState<TierFilter>('all'); const [tierFilter] = useState<TierFilter>('all');
const [availabilityFilter] = useState<AvailabilityFilter>('all'); const [availabilityFilter] = useState<AvailabilityFilter>('all');

View File

@@ -3,7 +3,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { SponsorLeagueDetailTemplate } from '@/templates/SponsorLeagueDetailTemplate'; 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 [activeTab, setActiveTab] = useState<'overview' | 'drivers' | 'races' | 'sponsor'>('overview');
const [selectedTier, setSelectedTier] = useState<'main' | 'secondary'>('main'); const [selectedTier, setSelectedTier] = useState<'main' | 'secondary'>('main');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import { Box } from './Box'; import { Box } from '@/ui/Box';
import { Image } from './Image'; import { Image } from '@/ui/Image';
import { Text } from './Text'; import { Text } from '@/ui/Text';
interface ActiveDriverCardProps { interface ActiveDriverCardProps {
name: string; 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 { Box } from '@/ui/Box';
import { Card } from '@/ui/Card'; import { Card } from '@/ui/Card';
import { GoalCard } from '@/ui/GoalCard'; import { GoalCard } from '@/ui/GoalCard';
import { Heading } from '@/ui/Heading'; import { Heading } from '@/ui/Heading';
import { MilestoneItem } from '@/ui/MilestoneItem'; import { MilestoneItem } from '@/components/achievements/MilestoneItem';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
interface Achievement { interface Achievement {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading'; import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon'; import { Icon } from '@/ui/Icon';
import { Image } from '@/ui/Image'; import { Image } from '@/ui/Image';
import { MedalBadge } from '@/ui/MedalBadge'; import { MedalBadge } from '@/components/leaderboards/MedalBadge';
import { MiniStat } from '@/ui/MiniStat'; import { MiniStat } from '@/ui/MiniStat';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { Flag, Shield, Star, TrendingUp } from 'lucide-react'; 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 { Box } from '@/ui/Box';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { CountryFlag } from '@/ui/CountryFlag'; import { CountryFlag } from '@/ui/CountryFlag';
import { DriverRatingPill } from '@/ui/DriverRatingPill'; import { DriverRatingPill } from '@/components/drivers/DriverRatingPill';
import { Heading } from '@/ui/Heading'; import { Heading } from '@/ui/Heading';
import { Image } from '@/ui/Image'; import { Image } from '@/ui/Image';
import { PlaceholderImage } from '@/ui/PlaceholderImage'; import { PlaceholderImage } from '@/ui/PlaceholderImage';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,9 +9,9 @@ import { Heading } from '@/ui/Heading';
import { Link } from '@/ui/Link'; import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { LeagueCard } from '@/ui/LeagueCard'; import { LeagueCard } from '@/components/leagues/LeagueCard';
import { TeamCard } from '@/ui/TeamCard'; import { TeamCard } from '@/components/teams/TeamCard';
import { UpcomingRaceItem } from '@/ui/UpcomingRaceItem'; import { UpcomingRaceItem } from '@/components/races/UpcomingRaceItem';
import { HomeViewData } from '@/templates/HomeTemplate'; import { HomeViewData } from '@/templates/HomeTemplate';
interface DiscoverySectionProps { 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 } from 'lucide-react';
import { Trophy, ChevronRight } from 'lucide-react';
import { Button } from '@/ui/Button';
import { Box } from '@/ui/Box'; import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text'; 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 { 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 { SkillLevelDisplay } from '@/lib/display-objects/SkillLevelDisplay';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay'; import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { RankMedal } from './RankMedal';
import { LeaderboardTableShell } from './LeaderboardTableShell';
interface DriverLeaderboardPreviewProps { interface DriverLeaderboardPreviewProps {
drivers: { drivers: {
@@ -32,54 +28,18 @@ export function DriverLeaderboardPreview({ drivers, onDriverClick, onNavigateToD
const top10 = drivers; // Already sliced in builder const top10 = drivers; // Already sliced in builder
return ( return (
<LeaderboardTableShell> <LeaderboardPreviewShell
<Box title="Driver Rankings"
display="flex" subtitle="Top Performers"
alignItems="center" onViewFull={onNavigateToDrivers}
justifyContent="between" icon={Trophy}
px={5} iconColor="var(--primary-blue)"
py={4} iconBgGradient="linear-gradient(to bottom right, rgba(25, 140, 255, 0.2), rgba(25, 140, 255, 0.1))"
borderBottom viewFullLabel="View All"
borderColor="border-charcoal-outline/50" >
bg="bg-deep-charcoal/40" <LeaderboardList>
>
<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}>
{top10.map((driver, index) => { {top10.map((driver, index) => {
const position = index + 1; const position = index + 1;
const isLast = index === top10.length - 1;
return ( return (
<Box <Box
@@ -97,11 +57,9 @@ export function DriverLeaderboardPreview({ drivers, onDriverClick, onNavigateToD
transition transition
hoverBg="bg-white/[0.02]" hoverBg="bg-white/[0.02]"
group group
borderBottom={!isLast}
borderColor="border-charcoal-outline/30"
> >
<Box w="8" display="flex" justifyContent="center"> <Box w="8" display="flex" justifyContent="center">
<RankMedal rank={position} size="sm" /> <RankBadge rank={position} />
</Box> </Box>
<Box <Box
@@ -111,7 +69,6 @@ export function DriverLeaderboardPreview({ drivers, onDriverClick, onNavigateToD
rounded="full" rounded="full"
overflow="hidden" overflow="hidden"
border border
borderWidth="1px"
borderColor="border-charcoal-outline" borderColor="border-charcoal-outline"
groupHoverBorderColor="primary-blue/50" groupHoverBorderColor="primary-blue/50"
transition transition
@@ -152,7 +109,7 @@ export function DriverLeaderboardPreview({ drivers, onDriverClick, onNavigateToD
</Box> </Box>
); );
})} })}
</Stack> </LeaderboardList>
</LeaderboardTableShell> </LeaderboardPreviewShell>
); );
} }

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/ui/Table'; import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/ui/Table';
import { RankingRow } from './RankingRow'; import { RankingRow } from './RankingRow';
import { LeaderboardTableShell } from './LeaderboardTableShell'; import { LeaderboardTableShell } from '@/ui/LeaderboardTableShell';
interface LeaderboardDriver { interface LeaderboardDriver {
id: string; id: string;
@@ -22,28 +22,23 @@ interface LeaderboardTableProps {
} }
export function LeaderboardTable({ drivers, onDriverClick }: 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 ( return (
<LeaderboardTableShell isEmpty={drivers.length === 0} emptyMessage="No drivers found"> <LeaderboardTableShell columns={columns}>
<Table> {drivers.map((driver) => (
<TableHead> <RankingRow
<TableRow> key={driver.id}
<TableHeader w="32">Rank</TableHeader> {...driver}
<TableHeader>Driver</TableHeader> onClick={() => onDriverClick?.(driver.id)}
<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> </LeaderboardTableShell>
); );
} }

View File

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

View File

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

View File

@@ -1,15 +1,12 @@
import React from 'react'; import { Users } from 'lucide-react';
import { Users, ChevronRight } from 'lucide-react';
import { Button } from '@/ui/Button';
import { Box } from '@/ui/Box'; import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text'; 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 { 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 { getMediaUrl } from '@/lib/utilities/media';
import { RankMedal } from './RankMedal'; import { Icon } from '@/ui/Icon';
import { LeaderboardTableShell } from './LeaderboardTableShell';
interface TeamLeaderboardPreviewProps { interface TeamLeaderboardPreviewProps {
teams: { teams: {
@@ -30,54 +27,18 @@ export function TeamLeaderboardPreview({ teams, onTeamClick, onNavigateToTeams }
const top5 = teams; const top5 = teams;
return ( return (
<LeaderboardTableShell> <LeaderboardPreviewShell
<Box title="Team Rankings"
display="flex" subtitle="Top Performing Teams"
alignItems="center" onViewFull={onNavigateToTeams}
justifyContent="between" icon={Users}
px={5} iconColor="var(--neon-purple)"
py={4} iconBgGradient="linear-gradient(to bottom right, rgba(168, 85, 247, 0.2), rgba(168, 85, 247, 0.1))"
borderBottom viewFullLabel="View All"
borderColor="border-charcoal-outline/50" >
bg="bg-deep-charcoal/40" <LeaderboardList>
> {top5.map((team) => {
<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) => {
const position = team.position; const position = team.position;
const isLast = index === top5.length - 1;
return ( return (
<Box <Box
@@ -95,11 +56,9 @@ export function TeamLeaderboardPreview({ teams, onTeamClick, onNavigateToTeams }
transition transition
hoverBg="bg-white/[0.02]" hoverBg="bg-white/[0.02]"
group group
borderBottom={!isLast}
borderColor="border-charcoal-outline/30"
> >
<Box w="8" display="flex" justifyContent="center"> <Box w="8" display="flex" justifyContent="center">
<RankMedal rank={position} size="sm" /> <RankBadge rank={position} />
</Box> </Box>
<Box <Box
@@ -166,7 +125,7 @@ export function TeamLeaderboardPreview({ teams, onTeamClick, onNavigateToTeams }
</Box> </Box>
); );
})} })}
</Stack> </LeaderboardList>
</LeaderboardTableShell> </LeaderboardPreviewShell>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,91 +1,171 @@
'use client'; 'use client';
import React from 'react'; import { ArrowRight, Car, ChevronRight, LucideIcon, Trophy, Zap } from 'lucide-react';
import { Clock, MapPin, Users } from 'lucide-react';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Icon } from '@/ui/Icon';
import { Box } from '@/ui/Box'; 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 { interface RaceCardProps {
id: string; track: string;
title: string; car: string;
leagueName: string;
trackName: string;
scheduledAt: string; scheduledAt: string;
entrantCount: number; status: string;
status: SessionStatus; leagueName: string;
onClick: (id: string) => void; leagueId?: string;
strengthOfField?: number | null;
onClick?: () => void;
statusConfig: {
border: string;
bg: string;
color: string;
icon: LucideIcon | null;
label: string;
};
} }
export function RaceCard({ export function RaceCard({
id, track,
title, car,
leagueName,
trackName,
scheduledAt, scheduledAt,
entrantCount,
status, status,
leagueName,
leagueId,
strengthOfField,
onClick, onClick,
statusConfig,
}: RaceCardProps) { }: RaceCardProps) {
const scheduledAtDate = new Date(scheduledAt);
return ( return (
<Box <Surface
as="article"
onClick={() => onClick(id)}
bg="bg-surface-charcoal" bg="bg-surface-charcoal"
rounded="xl"
border border
borderColor="border-outline-steel" borderColor="border-outline-steel"
p={4} padding={4}
onClick={onClick}
cursor={onClick ? 'pointer' : 'default'}
hoverBorderColor="border-primary-accent" hoverBorderColor="border-primary-accent"
transition transition
cursor="pointer"
position="relative" position="relative"
overflow="hidden" overflow="hidden"
group group
> >
{/* Hover Glow */} {/* Live indicator */}
<Box {status === 'running' && (
position="absolute" <Box
inset="0" position="absolute"
bg="bg-primary-accent" top="0"
bgOpacity={0.05} left="0"
opacity={0} right="0"
groupHoverOpacity={1} h="1"
transition bg="bg-success-green"
/> animate="pulse"
/>
)}
<Stack gap={4}> <Stack direction="row" align="start" gap={4}>
<Stack direction="row" justifyContent="between" alignItems="start"> {/* Time Column */}
<Stack gap={1}> <Box textAlign="center" flexShrink={0} width="16">
<Text size="xs" color="text-gray-500" weight="bold" uppercase> <Text size="lg" weight="bold" color="text-white" block>
{leagueName} {scheduledAtDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</Text> </Text>
<Text size="lg" weight="bold" groupHoverTextColor="text-primary-accent"> <Text size="xs" color={statusConfig.color} block>
{title} {status === 'running' ? 'LIVE' : scheduledAtDate.toLocaleDateString()}
</Text> </Text>
</Stack>
<SessionStatusBadge status={status} />
</Stack>
<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>
</Box> </Box>
<Stack direction="row" alignItems="center" gap={2} pt={2} borderTop borderColor="border-outline-steel" bgOpacity={0.5}> {/* Divider */}
<Icon icon={Users} size={3} color="#4ED4E0" /> <Box
<Text size="xs" color="text-gray-400"> w="px"
<Text as="span" color="text-telemetry-aqua" weight="bold">{entrantCount}</Text> ENTRANTS bg="border-outline-steel"
</Text> alignSelf="stretch"
</Stack> />
{/* 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> </Stack>
</Box> </Surface>
); );
} }

View File

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

View File

@@ -1,4 +1,4 @@
'use client';
import { Box } from '@/ui/Box'; import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
@@ -49,7 +49,7 @@ export function RaceFilterModal({
isOpen={isOpen} isOpen={isOpen}
onOpenChange={(open) => !open && onClose()} onOpenChange={(open) => !open && onClose()}
title="Filters" 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}> <Stack gap={4}>
{/* Search */} {/* Search */}
@@ -76,7 +76,7 @@ export function RaceFilterModal({
size="sm" size="sm"
onClick={() => setTimeFilter(filter)} 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)} {filter.charAt(0).toUpperCase() + filter.slice(1)}
</Button> </Button>
))} ))}

View File

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

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