website refactor
This commit is contained in:
24612
api-smoke-report.json
24612
api-smoke-report.json
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
5
apps/website/app/health/route.ts
Normal file
5
apps/website/app/health/route.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
39
apps/website/components/layout/AuthedNav.tsx
Normal file
39
apps/website/components/layout/AuthedNav.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
41
apps/website/components/layout/BrandMark.tsx
Normal file
41
apps/website/components/layout/BrandMark.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
49
apps/website/components/layout/HeaderActions.tsx
Normal file
49
apps/website/components/layout/HeaderActions.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
66
apps/website/components/layout/NavLink.tsx
Normal file
66
apps/website/components/layout/NavLink.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
38
apps/website/components/layout/PublicNav.tsx
Normal file
38
apps/website/components/layout/PublicNav.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
<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"
|
||||
>
|
||||
<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}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,19 +22,16 @@ 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>
|
||||
<LeaderboardTableShell columns={columns}>
|
||||
{drivers.map((driver) => (
|
||||
<RankingRow
|
||||
key={driver.id}
|
||||
@@ -42,8 +39,6 @@ export function LeaderboardTable({ drivers, onDriverClick }: LeaderboardTablePro
|
||||
onClick={() => onDriverClick?.(driver.id)}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</LeaderboardTableShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
<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"
|
||||
>
|
||||
<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) => {
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Stack } from './Stack';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
|
||||
interface JoinRequestListProps {
|
||||
children: ReactNode;
|
||||
@@ -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<{
|
||||
@@ -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}
|
||||
position="relative"
|
||||
display="flex"
|
||||
flexDirection="col"
|
||||
overflow="hidden"
|
||||
cursor={onClick ? 'pointer' : 'default'}
|
||||
h="full"
|
||||
onClick={onClick}
|
||||
className="group"
|
||||
>
|
||||
{/* Card Container */}
|
||||
<Box
|
||||
position="relative"
|
||||
h="full"
|
||||
rounded="none"
|
||||
bg="panel-gray/40"
|
||||
border
|
||||
borderColor="zinc-800"
|
||||
bg="zinc-900/50"
|
||||
hoverBorderColor="blue-500/30"
|
||||
hoverBg="zinc-900"
|
||||
borderColor="border-gray/50"
|
||||
overflow="hidden"
|
||||
transition
|
||||
cursor="pointer"
|
||||
group
|
||||
className="hover:border-primary-accent/30 hover:bg-panel-gray/60 transition-all duration-300"
|
||||
>
|
||||
{/* Cover Image */}
|
||||
<Box position="relative" h="32" overflow="hidden">
|
||||
<Box fullWidth fullHeight opacity={0.6}>
|
||||
<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)" />
|
||||
{/* Gradient Overlay */}
|
||||
<Box position="absolute" inset="0" bg="linear-gradient(to top, #0C0D0F, 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>
|
||||
{/* Badges - Top Left */}
|
||||
<Box position="absolute" top="3" left="3" display="flex" alignItems="center" gap={2}>
|
||||
{badges}
|
||||
</Box>
|
||||
|
||||
{/* Championship Type Badge - Top Right */}
|
||||
<Box position="absolute" top="3" right="3">
|
||||
{championshipBadge}
|
||||
</Box>
|
||||
|
||||
{/* 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} />
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
</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} />
|
||||
</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"
|
||||
>
|
||||
<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>
|
||||
<Text size="xs" color="text-zinc-500" lineClamp={2} leading="relaxed" h="8">
|
||||
</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>
|
||||
{/* 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" bg="zinc-800" overflow="hidden">
|
||||
<Box h="1" rounded="none" bg="border-gray/30" overflow="hidden">
|
||||
<Box
|
||||
h="full"
|
||||
rounded="none"
|
||||
transition
|
||||
bg={fillPercentage > 90 ? 'bg-amber-500' : 'bg-blue-500'}
|
||||
w={`${Math.min(fillPercentage, 100)}%`}
|
||||
bg={
|
||||
fillPercentage >= 90
|
||||
? 'warning-amber'
|
||||
: fillPercentage >= 70
|
||||
? 'primary-accent'
|
||||
: 'success-green'
|
||||
}
|
||||
style={{ width: `${Math.min(fillPercentage, 100)}%` }}
|
||||
/>
|
||||
</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'}
|
||||
{/* 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 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>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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" />
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
>
|
||||
<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={`/api/media/league-logo/${leagueId}`}
|
||||
alt={`${leagueName} logo`}
|
||||
src={logoUrl}
|
||||
alt={`${name} logo`}
|
||||
width={64}
|
||||
height={64}
|
||||
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>
|
||||
)}
|
||||
<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>
|
||||
<MembershipStatus leagueId={leagueId} />
|
||||
</Stack>
|
||||
{statusContent}
|
||||
</Box>
|
||||
{description && (
|
||||
<Text color="text-zinc-400" size="sm" maxWidth="2xl" block leading="relaxed">
|
||||
<Text color="text-gray-400" size="sm" maxWidth="xl" block>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 {
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
@@ -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 */}
|
||||
{/* Live indicator */}
|
||||
{status === 'running' && (
|
||||
<Box
|
||||
position="absolute"
|
||||
inset="0"
|
||||
bg="bg-primary-accent"
|
||||
bgOpacity={0.05}
|
||||
opacity={0}
|
||||
groupHoverOpacity={1}
|
||||
transition
|
||||
top="0"
|
||||
left="0"
|
||||
right="0"
|
||||
h="1"
|
||||
bg="bg-success-green"
|
||||
animate="pulse"
|
||||
/>
|
||||
)}
|
||||
|
||||
<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>
|
||||
|
||||
{/* Divider */}
|
||||
<Box
|
||||
w="px"
|
||||
bg="border-outline-steel"
|
||||
alignSelf="stretch"
|
||||
/>
|
||||
|
||||
<Stack gap={4}>
|
||||
<Stack direction="row" justifyContent="between" alignItems="start">
|
||||
<Stack gap={1}>
|
||||
<Text size="xs" color="text-gray-500" weight="bold" uppercase>
|
||||
{/* 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>
|
||||
<Text size="lg" weight="bold" groupHoverTextColor="text-primary-accent">
|
||||
{title}
|
||||
</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>
|
||||
<Icon icon={ArrowRight} size={3} color="var(--primary-accent)" />
|
||||
</Stack>
|
||||
</Link>
|
||||
</Box>
|
||||
</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>
|
||||
{/* Arrow */}
|
||||
<Icon
|
||||
icon={ChevronRight}
|
||||
size={5}
|
||||
color="var(--text-gray-500)"
|
||||
groupHoverTextColor="text-primary-accent"
|
||||
transition
|
||||
flexShrink={0}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
@@ -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
Reference in New Issue
Block a user