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
|
# 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
|
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 './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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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 {
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
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 } 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"
|
|
||||||
>
|
>
|
||||||
<Box display="flex" alignItems="center" gap={3}>
|
<LeaderboardList>
|
||||||
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,19 +22,16 @@ 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>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableHeader w="32">Rank</TableHeader>
|
|
||||||
<TableHeader>Driver</TableHeader>
|
|
||||||
<TableHeader textAlign="center">Races</TableHeader>
|
|
||||||
<TableHeader textAlign="center">Rating</TableHeader>
|
|
||||||
<TableHeader textAlign="center">Wins</TableHeader>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{drivers.map((driver) => (
|
{drivers.map((driver) => (
|
||||||
<RankingRow
|
<RankingRow
|
||||||
key={driver.id}
|
key={driver.id}
|
||||||
@@ -42,8 +39,6 @@ export function LeaderboardTable({ drivers, onDriverClick }: LeaderboardTablePro
|
|||||||
onClick={() => onDriverClick?.(driver.id)}
|
onClick={() => onDriverClick?.(driver.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</LeaderboardTableShell>
|
</LeaderboardTableShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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"
|
|
||||||
>
|
>
|
||||||
<Box display="flex" alignItems="center" gap={3}>
|
<LeaderboardList>
|
||||||
<Box
|
{top5.map((team) => {
|
||||||
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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<{
|
||||||
@@ -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}
|
||||||
|
className="group"
|
||||||
|
>
|
||||||
|
{/* Card Container */}
|
||||||
|
<Box
|
||||||
|
position="relative"
|
||||||
|
h="full"
|
||||||
|
rounded="none"
|
||||||
|
bg="panel-gray/40"
|
||||||
border
|
border
|
||||||
borderColor="zinc-800"
|
borderColor="border-gray/50"
|
||||||
bg="zinc-900/50"
|
overflow="hidden"
|
||||||
hoverBorderColor="blue-500/30"
|
|
||||||
hoverBg="zinc-900"
|
|
||||||
transition
|
transition
|
||||||
cursor="pointer"
|
className="hover:border-primary-accent/30 hover:bg-panel-gray/60 transition-all duration-300"
|
||||||
group
|
|
||||||
>
|
>
|
||||||
{/* Cover Image */}
|
{/* Cover Image */}
|
||||||
<Box position="relative" h="32" overflow="hidden">
|
<Box position="relative" h="32" overflow="hidden">
|
||||||
<Box fullWidth fullHeight opacity={0.6}>
|
|
||||||
<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 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} />
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
{/* Championship Icon */}
|
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
top="3"
|
|
||||||
right="3"
|
|
||||||
p={1.5}
|
|
||||||
bg="zinc-900/80"
|
|
||||||
color="text-zinc-400"
|
|
||||||
border
|
|
||||||
borderColor="white/10"
|
|
||||||
blur="sm"
|
|
||||||
>
|
|
||||||
{championshipType === 'driver' && <Trophy size={14} />}
|
|
||||||
{championshipType === 'team' && <Users size={14} />}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<Box position="relative" display="flex" flexDirection="col" flexGrow={1} p={4} pt={6}>
|
<Box pt={8} px={4} pb={4} display="flex" flexDirection="col" fullHeight>
|
||||||
{/* Logo */}
|
{/* Title & Description */}
|
||||||
<Box
|
<Stack direction="row" align="center" gap={2} mb={1}>
|
||||||
position="absolute"
|
<Box w="1" h="4" bg="primary-accent" />
|
||||||
top="-6"
|
<Heading level={3} fontSize="lg" weight="bold" className="line-clamp-1 group-hover:text-primary-accent transition-colors tracking-tight">
|
||||||
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"
|
|
||||||
>
|
|
||||||
{name}
|
{name}
|
||||||
</Heading>
|
</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'}
|
{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>
|
||||||
|
<Text size="xs" color="text-gray-400" font="mono">
|
||||||
|
{usedSlots}/{maxSlots || '∞'}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box h="1" bg="zinc-800" overflow="hidden">
|
<Box h="1" rounded="none" bg="border-gray/30" overflow="hidden">
|
||||||
<Box
|
<Box
|
||||||
h="full"
|
h="full"
|
||||||
|
rounded="none"
|
||||||
transition
|
transition
|
||||||
bg={fillPercentage > 90 ? 'bg-amber-500' : 'bg-blue-500'}
|
bg={
|
||||||
w={`${Math.min(fillPercentage, 100)}%`}
|
fillPercentage >= 90
|
||||||
|
? 'warning-amber'
|
||||||
|
: fillPercentage >= 70
|
||||||
|
? 'primary-accent'
|
||||||
|
: 'success-green'
|
||||||
|
}
|
||||||
|
style={{ width: `${Math.min(fillPercentage, 100)}%` }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box display="flex" alignItems="center" justifyContent="between" pt={3} borderTop borderColor="zinc-800/50">
|
{/* Open Slots Badge */}
|
||||||
<Box display="flex" alignItems="center" gap={2} color="text-zinc-500">
|
{hasOpenSlots && (
|
||||||
<Calendar size={12} />
|
<Box display="flex" alignItems="center" gap={1.5} px={2} py={1} rounded="none" bg="primary-accent/5" border borderColor="primary-accent/20">
|
||||||
<Text weight="bold" uppercase font="mono" fontSize="10px">
|
<Box w="1.5" h="1.5" rounded="full" bg="primary-accent" className="animate-pulse" />
|
||||||
{nextRaceDate || 'TBD'}
|
<Text size="xs" color="text-primary-accent" weight="bold" className="uppercase tracking-tighter">
|
||||||
|
{openSlotsCount} OPEN
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box display="flex" alignItems="center" gap={1} color="text-zinc-500"
|
)}
|
||||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
|
||||||
className="group-hover:text-blue-400 transition-colors"
|
|
||||||
>
|
|
||||||
<Text weight="bold" uppercase letterSpacing="widest" fontSize="10px">View</Text>
|
|
||||||
<Box
|
|
||||||
// eslint-disable-next-line gridpilot-rules/component-classification
|
|
||||||
className="transition-transform group-hover:translate-x-0.5"
|
|
||||||
>
|
|
||||||
<ChevronRight size={12} />
|
|
||||||
</Box>
|
</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>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -181,3 +190,4 @@ export function LeagueCard({
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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" />
|
||||||
@@ -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;
|
||||||
@@ -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"
|
|
||||||
h="20"
|
|
||||||
overflow="hidden"
|
|
||||||
border
|
|
||||||
borderColor="white/10"
|
|
||||||
bg="zinc-900"
|
|
||||||
shadow="2xl"
|
|
||||||
>
|
|
||||||
<Image
|
<Image
|
||||||
src={`/api/media/league-logo/${leagueId}`}
|
src={logoUrl}
|
||||||
alt={`${leagueName} logo`}
|
alt={`${name} logo`}
|
||||||
|
width={64}
|
||||||
|
height={64}
|
||||||
fullWidth
|
fullWidth
|
||||||
fullHeight
|
fullHeight
|
||||||
objectFit="cover"
|
objectFit="cover"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Stack gap={1}>
|
<Box>
|
||||||
<Stack direction="row" align="center" gap={4}>
|
<Box display="flex" alignItems="center" gap={3} mb={1}>
|
||||||
<Heading level={1} fontSize="3xl" weight="bold" color="text-white">
|
<Heading level={1}>
|
||||||
{leagueName}
|
{name}
|
||||||
{mainSponsor && (
|
{sponsorContent && (
|
||||||
<Text ml={3} size="lg" weight="normal" color="text-zinc-500">
|
<Text color="text-gray-400" weight="normal" size="lg" ml={2}>
|
||||||
by{' '}
|
by {sponsorContent}
|
||||||
{mainSponsor.websiteUrl ? (
|
|
||||||
<Box
|
|
||||||
as="a"
|
|
||||||
href={mainSponsor.websiteUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
color="text-blue-500"
|
|
||||||
hoverTextColor="text-blue-400"
|
|
||||||
transition
|
|
||||||
>
|
|
||||||
{mainSponsor.name}
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Text color="text-blue-500">{mainSponsor.name}</Text>
|
|
||||||
)}
|
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Heading>
|
</Heading>
|
||||||
<MembershipStatus leagueId={leagueId} />
|
{statusContent}
|
||||||
</Stack>
|
</Box>
|
||||||
{description && (
|
{description && (
|
||||||
<Text color="text-zinc-400" size="sm" maxWidth="2xl" block leading="relaxed">
|
<Text color="text-gray-400" size="sm" maxWidth="xl" block>
|
||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" />
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -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;
|
||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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 {
|
||||||
@@ -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 */}
|
||||||
|
{status === 'running' && (
|
||||||
<Box
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
inset="0"
|
top="0"
|
||||||
bg="bg-primary-accent"
|
left="0"
|
||||||
bgOpacity={0.05}
|
right="0"
|
||||||
opacity={0}
|
h="1"
|
||||||
groupHoverOpacity={1}
|
bg="bg-success-green"
|
||||||
transition
|
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}>
|
{/* Main Content */}
|
||||||
<Stack direction="row" justifyContent="between" alignItems="start">
|
<Box flex={1} minWidth="0">
|
||||||
<Stack gap={1}>
|
<Stack direction="row" align="start" justify="between" gap={4}>
|
||||||
<Text size="xs" color="text-gray-500" weight="bold" uppercase>
|
<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}
|
{leagueName}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="lg" weight="bold" groupHoverTextColor="text-primary-accent">
|
<Icon icon={ArrowRight} size={3} color="var(--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>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Stack direction="row" alignItems="center" gap={2} pt={2} borderTop borderColor="border-outline-steel" bgOpacity={0.5}>
|
{/* Arrow */}
|
||||||
<Icon icon={Users} size={3} color="#4ED4E0" />
|
<Icon
|
||||||
<Text size="xs" color="text-gray-400">
|
icon={ChevronRight}
|
||||||
<Text as="span" color="text-telemetry-aqua" weight="bold">{entrantCount}</Text> ENTRANTS
|
size={5}
|
||||||
</Text>
|
color="var(--text-gray-500)"
|
||||||
|
groupHoverTextColor="text-primary-accent"
|
||||||
|
transition
|
||||||
|
flexShrink={0}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Surface>
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
@@ -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
Reference in New Issue
Block a user