diff --git a/apps/website/components/achievements/AchievementCard.tsx b/apps/website/components/achievements/AchievementCard.tsx index bb3585e18..02e30b087 100644 --- a/apps/website/components/achievements/AchievementCard.tsx +++ b/apps/website/components/achievements/AchievementCard.tsx @@ -1,6 +1,7 @@ import { Card } from '@/ui/Card'; import { Text } from '@/ui/Text'; -import { Stack } from '@/ui/primitives/Stack'; +import { Group } from '@/ui/Group'; +import { DateDisplay } from '@/lib/display-objects/DateDisplay'; interface AchievementCardProps { title: string; @@ -10,13 +11,6 @@ interface AchievementCardProps { rarity: 'common' | 'rare' | 'epic' | 'legendary'; } -const rarityColors = { - common: 'border-gray-500 bg-gray-500/10', - rare: 'border-blue-400 bg-blue-400/10', - epic: 'border-purple-400 bg-purple-400/10', - legendary: 'border-warning-amber bg-warning-amber/10' -}; - export function AchievementCard({ title, description, @@ -24,27 +18,27 @@ export function AchievementCard({ unlockedAt, rarity, }: AchievementCardProps) { + const rarityVariantMap = { + common: 'rarity-common', + rare: 'rarity-rare', + epic: 'rarity-epic', + legendary: 'rarity-legendary' + } as const; + return ( - + {icon} - - {title} - {description} - - {new Date(unlockedAt).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' - })} + + {title} + {description} + + {DateDisplay.formatShort(unlockedAt)} - - + + ); } diff --git a/apps/website/components/achievements/AchievementGrid.tsx b/apps/website/components/achievements/AchievementGrid.tsx index c47c526fa..325e6ab8f 100644 --- a/apps/website/components/achievements/AchievementGrid.tsx +++ b/apps/website/components/achievements/AchievementGrid.tsx @@ -1,11 +1,12 @@ import { AchievementDisplay } from '@/lib/display-objects/AchievementDisplay'; import { Card } from '@/ui/Card'; -import { Grid } from '@/ui/primitives/Grid'; +import { Grid } from '@/ui/Grid'; import { Heading } from '@/ui/Heading'; import { Icon } from '@/ui/Icon'; -import { Stack } from '@/ui/primitives/Stack'; +import { Group } from '@/ui/Group'; import { Text } from '@/ui/Text'; import { Award, Crown, Medal, Star, Target, Trophy, Zap } from 'lucide-react'; +import React from 'react'; interface Achievement { id: string; @@ -35,44 +36,44 @@ function getAchievementIcon(icon: string) { export function AchievementGrid({ achievements }: AchievementGridProps) { return ( - - - }> - Achievements - - {achievements.length} earned - - + + + + + + Achievements + + + {achievements.length} earned + + {achievements.map((achievement) => { const AchievementIcon = getAchievementIcon(achievement.icon); - const rarity = AchievementDisplay.getRarityColor(achievement.rarity); + const rarity = AchievementDisplay.getRarityVariant(achievement.rarity); return ( - - - - - - {achievement.title} - {achievement.description} - - - {achievement.rarity.toUpperCase()} + + + + + + {achievement.title} + {achievement.description} + + + {achievement.rarity} - - + + {AchievementDisplay.formatDate(achievement.earnedAt)} - - - + + + ); })} diff --git a/apps/website/components/achievements/MilestoneItem.tsx b/apps/website/components/achievements/MilestoneItem.tsx index 737bfa565..648d733de 100644 --- a/apps/website/components/achievements/MilestoneItem.tsx +++ b/apps/website/components/achievements/MilestoneItem.tsx @@ -1,5 +1,7 @@ import { Text } from '@/ui/Text'; -import { Stack } from '@/ui/primitives/Stack'; +import { Group } from '@/ui/Group'; +import { Card } from '@/ui/Card'; +import React from 'react'; interface MilestoneItemProps { label: string; @@ -9,12 +11,14 @@ interface MilestoneItemProps { export function MilestoneItem({ label, value, icon }: MilestoneItemProps) { return ( - - - {icon} - {label} - - {value} - + + + + {icon} + {label} + + {value} + + ); } diff --git a/apps/website/components/actions/ActionFiltersBar.tsx b/apps/website/components/actions/ActionFiltersBar.tsx index b5ee1d200..ab2215688 100644 --- a/apps/website/components/actions/ActionFiltersBar.tsx +++ b/apps/website/components/actions/ActionFiltersBar.tsx @@ -1,27 +1,25 @@ 'use client'; import { Input } from '@/ui/Input'; -import { Stack } from '@/ui/primitives/Stack'; +import { ControlBar } from '@/ui/ControlBar'; import { Select } from '@/ui/Select'; import { Text } from '@/ui/Text'; +import { Group } from '@/ui/Group'; import { useState } from 'react'; export function ActionFiltersBar() { const [filter, setFilter] = useState('all'); return ( - + } > - - Filter: + + Filter: {}} fullWidth={false} /> - - - - - + + ); } diff --git a/apps/website/components/actions/ActionList.tsx b/apps/website/components/actions/ActionList.tsx index 5025e60be..355b8e098 100644 --- a/apps/website/components/actions/ActionList.tsx +++ b/apps/website/components/actions/ActionList.tsx @@ -28,19 +28,19 @@ export function ActionList({ actions }: ActionListProps) { clickable > - {action.timestamp} + {action.timestamp} - {action.type} + {action.type} - {action.initiator} + {action.initiator} - + {action.details} diff --git a/apps/website/components/actions/ActionsHeader.tsx b/apps/website/components/actions/ActionsHeader.tsx index 2e4e8549f..f71c75ae6 100644 --- a/apps/website/components/actions/ActionsHeader.tsx +++ b/apps/website/components/actions/ActionsHeader.tsx @@ -1,9 +1,10 @@ 'use client'; -import { Stack } from '@/ui/primitives/Stack'; +import { Header } from '@/ui/Header'; import { StatusIndicator } from '@/ui/StatusIndicator'; import { Text } from '@/ui/Text'; import { Activity } from 'lucide-react'; +import React from 'react'; interface ActionsHeaderProps { title: string; @@ -11,31 +12,15 @@ interface ActionsHeaderProps { export function ActionsHeader({ title }: ActionsHeaderProps) { return ( - } > - - {null} - +
+
+ {title} - - - - - +
+ ); } diff --git a/apps/website/components/admin/AdminDangerZonePanel.tsx b/apps/website/components/admin/AdminDangerZonePanel.tsx index e69b519bb..b2d689dde 100644 --- a/apps/website/components/admin/AdminDangerZonePanel.tsx +++ b/apps/website/components/admin/AdminDangerZonePanel.tsx @@ -1,9 +1,6 @@ 'use client'; -import { Card } from '@/ui/Card'; -import { Heading } from '@/ui/Heading'; -import { Stack } from '@/ui/primitives/Stack'; -import { Text } from '@/ui/Text'; +import { DangerZone } from '@/ui/DangerZone'; import React from 'react'; interface AdminDangerZonePanelProps { @@ -24,20 +21,11 @@ export function AdminDangerZonePanel({ children }: AdminDangerZonePanelProps) { return ( - - - - - {title} - - - {description} - - - - {children} - - - + + {children} + ); } diff --git a/apps/website/components/admin/AdminDataTable.tsx b/apps/website/components/admin/AdminDataTable.tsx index 817ef1f83..ed7f0166e 100644 --- a/apps/website/components/admin/AdminDataTable.tsx +++ b/apps/website/components/admin/AdminDataTable.tsx @@ -1,7 +1,6 @@ 'use client'; import { Card } from '@/ui/Card'; -import { Stack } from '@/ui/primitives/Stack'; import React from 'react'; interface AdminDataTableProps { @@ -20,13 +19,15 @@ export function AdminDataTable({ maxHeight }: AdminDataTableProps) { return ( - - +
{children} - +
); } diff --git a/apps/website/components/admin/AdminEmptyState.tsx b/apps/website/components/admin/AdminEmptyState.tsx index cf13728ce..ba3873ba2 100644 --- a/apps/website/components/admin/AdminEmptyState.tsx +++ b/apps/website/components/admin/AdminEmptyState.tsx @@ -1,8 +1,6 @@ 'use client'; -import { Icon } from '@/ui/Icon'; -import { Stack } from '@/ui/primitives/Stack'; -import { Text } from '@/ui/Text'; +import { EmptyState } from '@/ui/EmptyState'; import { LucideIcon } from 'lucide-react'; import React from 'react'; @@ -26,23 +24,13 @@ export function AdminEmptyState({ action }: AdminEmptyStateProps) { return ( - - - - - {title} - - {description && ( - - {description} - - )} - - {action && ( - - {action} - - )} - + + {action} + ); } diff --git a/apps/website/components/admin/AdminHeaderPanel.tsx b/apps/website/components/admin/AdminHeaderPanel.tsx index 546e8a595..cde7c0fa8 100644 --- a/apps/website/components/admin/AdminHeaderPanel.tsx +++ b/apps/website/components/admin/AdminHeaderPanel.tsx @@ -1,9 +1,7 @@ 'use client'; -import { ProgressLine } from '@/components/shared/ux/ProgressLine'; -import { Heading } from '@/ui/Heading'; -import { Stack } from '@/ui/primitives/Stack'; -import { Text } from '@/ui/Text'; +import { ProgressLine } from '@/ui/ProgressLine'; +import { SectionHeader } from '@/ui/SectionHeader'; import React from 'react'; interface AdminHeaderPanelProps { @@ -26,27 +24,11 @@ export function AdminHeaderPanel({ isLoading = false }: AdminHeaderPanelProps) { return ( - - - - - {title} - - {description && ( - - {description} - - )} - - {actions && ( - - {actions} - - )} - - - - - + } + /> ); } diff --git a/apps/website/components/admin/AdminSectionHeader.tsx b/apps/website/components/admin/AdminSectionHeader.tsx index 64966fce8..4d24bc583 100644 --- a/apps/website/components/admin/AdminSectionHeader.tsx +++ b/apps/website/components/admin/AdminSectionHeader.tsx @@ -1,8 +1,6 @@ 'use client'; -import { Heading } from '@/ui/Heading'; -import { Stack } from '@/ui/primitives/Stack'; -import { Text } from '@/ui/Text'; +import { SectionHeader } from '@/ui/SectionHeader'; import React from 'react'; interface AdminSectionHeaderProps { @@ -23,22 +21,11 @@ export function AdminSectionHeader({ actions }: AdminSectionHeaderProps) { return ( - - - - {title} - - {description && ( - - {description} - - )} - - {actions && ( - - {actions} - - )} - + ); } diff --git a/apps/website/components/admin/AdminStatsPanel.tsx b/apps/website/components/admin/AdminStatsPanel.tsx index 3874f470b..993b3b29a 100644 --- a/apps/website/components/admin/AdminStatsPanel.tsx +++ b/apps/website/components/admin/AdminStatsPanel.tsx @@ -1,14 +1,14 @@ 'use client'; -import { Grid } from '@/ui/primitives/Grid'; -import { StatCard } from '@/ui/StatCard'; +import { StatGrid } from '@/ui/StatGrid'; import { LucideIcon } from 'lucide-react'; +import React from 'react'; interface AdminStat { label: string; value: string | number; icon: LucideIcon; - variant?: 'blue' | 'purple' | 'green' | 'orange'; + intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry'; trend?: { value: number; isPositive: boolean; @@ -27,18 +27,10 @@ interface AdminStatsPanelProps { */ export function AdminStatsPanel({ stats }: AdminStatsPanelProps) { return ( - - {stats.map((stat, index) => ( - - ))} - + ); } diff --git a/apps/website/components/admin/AdminToolbar.tsx b/apps/website/components/admin/AdminToolbar.tsx index 2a8d454f1..34ec81ce6 100644 --- a/apps/website/components/admin/AdminToolbar.tsx +++ b/apps/website/components/admin/AdminToolbar.tsx @@ -1,7 +1,6 @@ 'use client'; -import { Card } from '@/ui/Card'; -import { Stack } from '@/ui/primitives/Stack'; +import { ControlBar } from '@/ui/ControlBar'; import React from 'react'; interface AdminToolbarProps { @@ -20,17 +19,11 @@ export function AdminToolbar({ leftContent }: AdminToolbarProps) { return ( - - - {leftContent && ( - - {leftContent} - - )} - - {children} - - - + + {children} + ); } diff --git a/apps/website/components/admin/AdminUsersTable.tsx b/apps/website/components/admin/AdminUsersTable.tsx index 3ee05b275..9a7c4ee1b 100644 --- a/apps/website/components/admin/AdminUsersTable.tsx +++ b/apps/website/components/admin/AdminUsersTable.tsx @@ -3,9 +3,10 @@ import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { AdminUsersViewData } from '@/lib/view-data/AdminUsersViewData'; import { Button } from '@/ui/Button'; -import { Icon } from '@/ui/Icon'; -import { Stack } from '@/ui/primitives/Stack'; +import { IconButton } from '@/ui/IconButton'; import { SimpleCheckbox } from '@/ui/SimpleCheckbox'; +import { Badge } from '@/ui/Badge'; +import { DriverIdentity } from '@/ui/DriverIdentity'; import { Table, TableBody, @@ -15,8 +16,9 @@ import { TableRow } from '@/ui/Table'; import { Text } from '@/ui/Text'; -import { MoreVertical, Shield, Trash2 } from 'lucide-react'; +import { MoreVertical, Trash2 } from 'lucide-react'; import { UserStatusTag } from './UserStatusTag'; +import React from 'react'; interface AdminUsersTableProps { users: AdminUsersViewData['users']; @@ -49,7 +51,7 @@ export function AdminUsersTable({ - + - - - - - - - {user.displayName} - - - {user.email} - - - + - +
{user.roles.map((role) => ( - - - {role} - - + + {role} + ))} - +
- + {user.lastLoginAt ? DateDisplay.formatShort(user.lastLoginAt) : 'Never'} - +
{user.status === 'active' ? ( + icon={Trash2} + title="Delete" + /> - - + icon={MoreVertical} + title="More" + /> +
))} diff --git a/apps/website/components/admin/BulkActionBar.tsx b/apps/website/components/admin/BulkActionBar.tsx index d7fe793d6..7f72bbed6 100644 --- a/apps/website/components/admin/BulkActionBar.tsx +++ b/apps/website/components/admin/BulkActionBar.tsx @@ -1,9 +1,7 @@ 'use client'; import { Button } from '@/ui/Button'; -import { Stack } from '@/ui/primitives/Stack'; -import { Text } from '@/ui/Text'; -import { AnimatePresence, motion } from 'framer-motion'; +import { BulkActions } from '@/ui/BulkActions'; import React from 'react'; interface BulkActionBarProps { @@ -28,65 +26,28 @@ export function BulkActionBar({ onClearSelection }: BulkActionBarProps) { return ( - - {selectedCount > 0 && ( - 0} + > + {actions.map((action) => ( + - ))} - - - - - )} - + {action.label} + + ))} + + ); } diff --git a/apps/website/components/admin/UserFilters.tsx b/apps/website/components/admin/UserFilters.tsx index 2e2c3782d..f4e60d6d6 100644 --- a/apps/website/components/admin/UserFilters.tsx +++ b/apps/website/components/admin/UserFilters.tsx @@ -3,11 +3,11 @@ import { Button } from '@/ui/Button'; import { Icon } from '@/ui/Icon'; import { Input } from '@/ui/Input'; -import { Stack } from '@/ui/primitives/Stack'; import { Select } from '@/ui/Select'; import { Text } from '@/ui/Text'; import { Filter, Search } from 'lucide-react'; import { AdminToolbar } from './AdminToolbar'; +import React from 'react'; interface UserFiltersProps { search: string; @@ -31,9 +31,9 @@ export function UserFilters({ return ( - - Filters + + + Filters {(search || roleFilter || statusFilter) && (
- Type + Type - Description + Description - Time + Time - Status + Status @@ -53,16 +45,16 @@ export function RecentActivityTable({ items }: RecentActivityTableProps) { {items.map((item) => ( - {item.type} + {item.type} - {item.description} + {item.description} - {item.timestamp} + {item.timestamp} - + ))} diff --git a/apps/website/components/dashboard/TelemetryPanel.tsx b/apps/website/components/dashboard/TelemetryPanel.tsx index 6f657a0ec..4486caffe 100644 --- a/apps/website/components/dashboard/TelemetryPanel.tsx +++ b/apps/website/components/dashboard/TelemetryPanel.tsx @@ -1,5 +1,5 @@ import { Panel } from '@/ui/Panel'; -import { Stack } from '@/ui/primitives/Stack'; +import { Text } from '@/ui/Text'; import React from 'react'; interface TelemetryPanelProps { @@ -11,14 +11,13 @@ interface TelemetryPanelProps { * TelemetryPanel * * A dense, instrument-grade panel for displaying data and controls. - * Uses UI primitives to comply with architectural constraints. */ export function TelemetryPanel({ title, children }: TelemetryPanelProps) { return ( - + {children} - + ); } diff --git a/apps/website/components/dev/DebugModeToggle.tsx b/apps/website/components/dev/DebugModeToggle.tsx index 5d8b680fe..8a5dbc40d 100644 --- a/apps/website/components/dev/DebugModeToggle.tsx +++ b/apps/website/components/dev/DebugModeToggle.tsx @@ -4,11 +4,12 @@ import type { GlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler'; import { Button } from '@/ui/Button'; import { Icon } from '@/ui/Icon'; -import { Grid } from '@/ui/primitives/Grid'; -import { Stack } from '@/ui/primitives/Stack'; import { Text } from '@/ui/Text'; -import { Bug, Shield, X } from 'lucide-react'; -import { useCallback, useEffect, useState } from 'react'; +import { FloatingAction } from '@/ui/FloatingAction'; +import { DebugPanel } from '@/ui/DebugPanel'; +import { StatGrid } from '@/ui/StatGrid'; +import { Bug, Shield } from 'lucide-react'; +import React, { useCallback, useEffect, useState } from 'react'; // Extend Window interface for debug globals declare global { @@ -187,141 +188,94 @@ export function DebugModeToggle({ show }: DebugModeToggleProps) { } return ( - + {/* Main Toggle Button */} {!isOpen && ( - + )} {/* Debug Panel */} {isOpen && ( - - {/* Header */} - - - - Debug Control - - - + setIsOpen(false)} + icon={} + > +
+ {/* Debug Toggle */} +
+
+ + Debug Mode +
+ +
- {/* Content */} - - - {/* Debug Toggle */} - - - - Debug Mode - - - + {/* Metrics */} + {debugEnabled && ( + + )} - {/* Metrics */} - {debugEnabled && ( - - - Errors - {metrics.errors} - - - API - {metrics.apiRequests} - - - Failures - {metrics.apiFailures} - - - )} + {/* Actions */} + {debugEnabled && ( +
+ Test Actions +
+ + +
- {/* Actions */} - {debugEnabled && ( - - Test Actions - - - - + Utilities +
+ + +
+
+ )} - Utilities - - - - -
- )} + {/* Quick Links */} + {debugEnabled && ( +
+ Quick Access +
+
• window.__GRIDPILOT_GLOBAL_HANDLER__
+
• window.__GRIDPILOT_API_LOGGER__
+
• window.__GRIDPILOT_REACT_ERRORS__
+
+
+ )} - {/* Quick Links */} - {debugEnabled && ( - - Quick Access - - • window.__GRIDPILOT_GLOBAL_HANDLER__ - • window.__GRIDPILOT_API_LOGGER__ - • window.__GRIDPILOT_REACT_ERRORS__ - - - )} - - {/* Status */} - - - {debugEnabled ? 'Debug features active' : 'Debug mode disabled'} - {isDev && ' • Development Environment'} - - -
- - + {/* Status */} +
+ + {debugEnabled ? 'Debug features active' : 'Debug mode disabled'} + {isDev && ' • Development Environment'} + +
+
+
)} -
+
); } diff --git a/apps/website/components/drivers/ActiveDriverCard.tsx b/apps/website/components/drivers/ActiveDriverCard.tsx deleted file mode 100644 index a2c724ff6..000000000 --- a/apps/website/components/drivers/ActiveDriverCard.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Image } from '@/ui/Image'; -import { Stack } from '@/ui/primitives/Stack'; -import { Text } from '@/ui/Text'; - - -export function ActiveDriverCard({ - name, - avatarUrl, - categoryLabel, - categoryColor, - skillLevelLabel, - skillLevelColor, - onClick, -}: ActiveDriverCardProps) { - return ( - - - - {null} - - - {name} - - - {categoryLabel && ( - {categoryLabel} - )} - {skillLevelLabel && ( - {skillLevelLabel} - )} - - - ); -} diff --git a/apps/website/components/drivers/DriverIdentity.tsx b/apps/website/components/drivers/DriverIdentity.tsx deleted file mode 100644 index f28b59b72..000000000 --- a/apps/website/components/drivers/DriverIdentity.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { Badge } from '@/ui/Badge'; -import { Image } from '@/ui/Image'; -import { Link } from '@/ui/Link'; -import { PlaceholderImage } from '@/ui/PlaceholderImage'; -import { Box } from '@/ui/primitives/Box'; -import { Text } from '@/ui/Text'; - -export interface DriverIdentityProps { - driver: { - id: string; - name: string; - avatarUrl: string | null; - }; - href?: string; - contextLabel?: React.ReactNode; - meta?: React.ReactNode; - size?: 'sm' | 'md'; -} - -export function DriverIdentity(props: DriverIdentityProps) { - const { driver, href, contextLabel, meta, size = 'md' } = props; - - const avatarSize = size === 'sm' ? 40 : 48; - const nameSize = size === 'sm' ? 'sm' : 'base'; - - const avatarUrl = driver.avatarUrl; - - const content = ( - - - {avatarUrl ? ( - - ) : ( - - )} - - - - - - {driver.name} - - {contextLabel && ( - - {contextLabel} - - )} - - {meta && ( - - {meta} - - )} - - - ); - - if (href) { - return ( - - {content} - - ); - } - - return {content}; -} diff --git a/apps/website/components/drivers/DriverRankings.tsx b/apps/website/components/drivers/DriverRankings.tsx index acc198ce0..16b71721d 100644 --- a/apps/website/components/drivers/DriverRankings.tsx +++ b/apps/website/components/drivers/DriverRankings.tsx @@ -1,9 +1,10 @@ +'use client'; + import React from 'react'; -import { Card } from '@/ui/Card'; -import { Heading } from '@/ui/Heading'; +import { Panel } from '@/ui/Panel'; import { RankingListItem } from '@/components/leaderboards/RankingListItem'; import { RankingList } from '@/components/leaderboards/RankingList'; -import { MinimalEmptyState } from '@/components/shared/state/EmptyState'; +import { EmptyState } from '@/ui/EmptyState'; export interface DriverRanking { type: 'overall' | 'league'; @@ -21,19 +22,18 @@ interface DriverRankingsProps { export function DriverRankings({ rankings }: DriverRankingsProps) { if (!rankings || rankings.length === 0) { return ( - - Rankings - + - + ); } return ( - - Rankings + {rankings.map((ranking, index) => ( ))} - + ); } diff --git a/apps/website/components/drivers/ProfileRaceHistory.tsx b/apps/website/components/drivers/ProfileRaceHistory.tsx index c0fe2dbf4..3998835f3 100644 --- a/apps/website/components/drivers/ProfileRaceHistory.tsx +++ b/apps/website/components/drivers/ProfileRaceHistory.tsx @@ -1,14 +1,15 @@ 'use client'; -import { EmptyState } from '@/components/shared/state/EmptyState'; -import { LoadingWrapper } from '@/components/shared/state/LoadingWrapper'; +import { EmptyState } from '@/ui/EmptyState'; +import { LoadingWrapper } from '@/ui/LoadingWrapper'; import { Button } from '@/ui/Button'; import { Card } from '@/ui/Card'; import { Pagination } from '@/ui/Pagination'; -import { Stack } from '@/ui/primitives/Stack'; import { Text } from '@/ui/Text'; +import { ControlBar } from '@/ui/ControlBar'; import { Trophy } from 'lucide-react'; import { useEffect, useState } from 'react'; +import React from 'react'; interface RaceHistoryProps { driverId: string; @@ -24,7 +25,6 @@ export function ProfileRaceHistory({ driverId }: RaceHistoryProps) { async function loadRaceHistory() { try { // Driver race history is not exposed via API yet. - // Keep as placeholder until an endpoint exists. } catch (err) { console.error('Failed to load race history:', err); } finally { @@ -40,18 +40,7 @@ export function ProfileRaceHistory({ driverId }: RaceHistoryProps) { const totalPages = Math.ceil(filteredResults.length / resultsPerPage); if (loading) { - return ( - - - {[1, 2, 3].map(i => ( - - ))} - - - - - - ); + return ; } if (filteredResults.length === 0) { @@ -60,50 +49,50 @@ export function ProfileRaceHistory({ driverId }: RaceHistoryProps) { icon={Trophy} title="No race history yet" description="Complete races to build your racing record" + variant="minimal" /> ); } return ( - - - - - - +
+ +
+ + + +
+
- {/* No results until API provides driver results */} - - No results found for the selected filter. - +
+ No results found for the selected filter. +
- +
); } diff --git a/apps/website/components/errors/ApiErrorBoundary.tsx b/apps/website/components/errors/ApiErrorBoundary.tsx index d942d27aa..b3b0cc044 100644 --- a/apps/website/components/errors/ApiErrorBoundary.tsx +++ b/apps/website/components/errors/ApiErrorBoundary.tsx @@ -3,8 +3,8 @@ import React, { Component, ReactNode, useState } from 'react'; import { ApiError } from '@/lib/api/base/ApiError'; import { connectionMonitor } from '@/lib/api/base/ApiConnectionMonitor'; -import { ErrorDisplay } from '@/components/shared/state/ErrorDisplay'; -import { DevErrorPanel } from './DevErrorPanel'; +import { ErrorDisplay } from '@/ui/ErrorDisplay'; +import { DevErrorPanel } from '@/ui/DevErrorPanel'; interface Props { children: ReactNode; diff --git a/apps/website/components/errors/AppErrorBoundaryView.tsx b/apps/website/components/errors/AppErrorBoundaryView.tsx index 9878b8e4b..7ea00af24 100644 --- a/apps/website/components/errors/AppErrorBoundaryView.tsx +++ b/apps/website/components/errors/AppErrorBoundaryView.tsx @@ -2,7 +2,6 @@ import { Heading } from '@/ui/Heading'; import { Icon } from '@/ui/Icon'; -import { Stack } from '@/ui/primitives/Stack'; import { Text } from '@/ui/Text'; import { AlertTriangle } from 'lucide-react'; import React from 'react'; @@ -21,32 +20,30 @@ interface AppErrorBoundaryViewProps { */ export function AppErrorBoundaryView({ title, description, children }: AppErrorBoundaryViewProps) { return ( - +
{/* Header Icon */} - - - + +
{/* Typography */} - +
- - {title} - + {title} - + {description} - +
{children} -
+ ); } diff --git a/apps/website/components/errors/DevErrorPanel.tsx b/apps/website/components/errors/DevErrorPanel.tsx deleted file mode 100644 index 3edd109c6..000000000 --- a/apps/website/components/errors/DevErrorPanel.tsx +++ /dev/null @@ -1,382 +0,0 @@ -'use client'; - -import { connectionMonitor } from '@/lib/api/base/ApiConnectionMonitor'; -import { ApiError } from '@/lib/api/base/ApiError'; -import { CircuitBreakerRegistry } from '@/lib/api/base/RetryHandler'; -import { Badge } from '@/ui/Badge'; -import { Button } from '@/ui/Button'; -import { Card } from '@/ui/Card'; -import { Heading } from '@/ui/Heading'; -import { Icon } from '@/ui/Icon'; -import { Grid } from '@/ui/primitives/Grid'; -import { Stack } from '@/ui/primitives/Stack'; -import { Text } from '@/ui/Text'; -import { Activity, AlertTriangle, Copy, RefreshCw, Terminal, X } from 'lucide-react'; -import { useEffect, useState } from 'react'; - -interface DevErrorPanelProps { - error: ApiError; - onReset: () => void; -} - -/** - * Developer-focused error panel with detailed debugging information - */ -export function DevErrorPanel({ error, onReset }: DevErrorPanelProps) { - const [connectionStatus, setConnectionStatus] = useState(connectionMonitor.getHealth()); - const [circuitBreakers, setCircuitBreakers] = useState(CircuitBreakerRegistry.getInstance().getStatus()); - const [copied, setCopied] = useState(false); - - useEffect(() => { - // Update status on mount - const health = connectionMonitor.getHealth(); - setConnectionStatus(health); - setCircuitBreakers(CircuitBreakerRegistry.getInstance().getStatus()); - - // Listen for status changes - const handleStatusChange = () => { - setConnectionStatus(connectionMonitor.getHealth()); - setCircuitBreakers(CircuitBreakerRegistry.getInstance().getStatus()); - }; - - connectionMonitor.on('success', handleStatusChange); - connectionMonitor.on('failure', handleStatusChange); - connectionMonitor.on('connected', handleStatusChange); - connectionMonitor.on('disconnected', handleStatusChange); - connectionMonitor.on('degraded', handleStatusChange); - - return () => { - connectionMonitor.off('success', handleStatusChange); - connectionMonitor.off('failure', handleStatusChange); - connectionMonitor.off('connected', handleStatusChange); - connectionMonitor.off('disconnected', handleStatusChange); - connectionMonitor.off('degraded', handleStatusChange); - }; - }, []); - - const copyToClipboard = async () => { - const debugInfo = { - error: { - type: error.type, - message: error.message, - context: error.context, - stack: error.stack, - }, - connection: connectionStatus, - circuitBreakers, - timestamp: new Date().toISOString(), - userAgent: navigator.userAgent, - url: window.location.href, - }; - - try { - await navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2)); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch (err) { - // Silent failure for clipboard operations - } - }; - - const triggerHealthCheck = async () => { - await connectionMonitor.performHealthCheck(); - setConnectionStatus(connectionMonitor.getHealth()); - }; - - const resetCircuitBreakers = () => { - CircuitBreakerRegistry.getInstance().resetAll(); - setCircuitBreakers(CircuitBreakerRegistry.getInstance().getStatus()); - }; - - const getSeverityVariant = (): 'danger' | 'warning' | 'info' | 'default' => { - switch (error.getSeverity()) { - case 'error': return 'danger'; - case 'warn': return 'warning'; - case 'info': return 'info'; - default: return 'default'; - } - }; - - const reliability = connectionMonitor.getReliability(); - - return ( - - - - {/* Header */} - - - - API Error Debug Panel - - {error.type} - - - - - - - - - {/* Error Details */} - - - - - - Error Details - - - - - Type: - {error.type} - - - Message: - {error.message} - - - Endpoint: - {error.context.endpoint || 'N/A'} - - - Method: - {error.context.method || 'N/A'} - - - Status: - {error.context.statusCode || 'N/A'} - - - Retry Count: - {error.context.retryCount || 0} - - - Timestamp: - {error.context.timestamp} - - - Retryable: - - {error.isRetryable() ? 'Yes' : 'No'} - - - - Connectivity: - - {error.isConnectivityIssue() ? 'Yes' : 'No'} - - - {error.context.troubleshooting && ( - - Troubleshoot: - {error.context.troubleshooting} - - )} - - - - - {/* Connection Status */} - - - - Connection Health - - - - - Status: - - {connectionStatus.status.toUpperCase()} - - - - Reliability: - {reliability.toFixed(2)}% - - - Total Requests: - {connectionStatus.totalRequests} - - - Successful: - {connectionStatus.successfulRequests} - - - Failed: - {connectionStatus.failedRequests} - - - Consecutive Failures: - {connectionStatus.consecutiveFailures} - - - Avg Response: - {connectionStatus.averageResponseTime.toFixed(2)}ms - - - Last Check: - - {connectionStatus.lastCheck?.toLocaleTimeString() || 'Never'} - - - - - - - - {/* Right Column */} - - {/* Circuit Breakers */} - - - - Circuit Breakers - - - {Object.keys(circuitBreakers).length === 0 ? ( - - No circuit breakers active - - ) : ( - - {Object.entries(circuitBreakers).map(([endpoint, status]) => ( - - {endpoint} - - - {status.state} - - - {status.failures} failures - - ))} - - )} - - - - {/* Actions */} - - - Actions - - - - - - - - - - - {/* Quick Fixes */} - - - Quick Fixes - - - - Common solutions: - - Check API server is running - Verify CORS configuration - Check environment variables - Review network connectivity - Check API rate limits - - - - - - {/* Raw Error */} - - - Raw Error - - - - {JSON.stringify({ - type: error.type, - message: error.message, - context: error.context, - }, null, 2)} - - - - - - - {/* Console Output */} - - - - Console Output - - - {'>'} {error.getDeveloperMessage()} - Check browser console for full stack trace and additional debug info. - - - - - - ); -} diff --git a/apps/website/components/errors/ErrorDetails.tsx b/apps/website/components/errors/ErrorDetails.tsx index 8443718bf..a3ef5526a 100644 --- a/apps/website/components/errors/ErrorDetails.tsx +++ b/apps/website/components/errors/ErrorDetails.tsx @@ -3,10 +3,10 @@ import { Button } from '@/ui/Button'; import { Card } from '@/ui/Card'; import { Icon } from '@/ui/Icon'; -import { Stack } from '@/ui/primitives/Stack'; import { Text } from '@/ui/Text'; -import { ChevronDown, ChevronUp, Copy, Terminal } from 'lucide-react'; -import { useState } from 'react'; +import { Accordion } from '@/ui/Accordion'; +import { Copy } from 'lucide-react'; +import React, { useState } from 'react'; interface ErrorDetailsProps { error: Error & { digest?: string }; @@ -19,7 +19,6 @@ interface ErrorDetailsProps { * Part of the 500 route redesign. */ export function ErrorDetails({ error }: ErrorDetailsProps) { - const [showDetails, setShowDetails] = useState(false); const [copied, setCopied] = useState(false); const copyError = async () => { @@ -41,62 +40,28 @@ export function ErrorDetails({ error }: ErrorDetailsProps) { }; return ( - - setShowDetails(!showDetails)} - direction="row" - align="center" - justify="center" - gap={2} - color="text-gray-500" - className="transition-all hover:text-gray-300" - > - - - {showDetails ? 'Hide Technical Logs' : 'Show Technical Logs'} - - {showDetails ? : } - - - {showDetails && ( - - - +
+ +
+ + {error.stack || 'No stack trace available'} {error.digest && `\n\nDigest: ${error.digest}`} - +
- - - )} - +
+
+
+
); } diff --git a/apps/website/components/errors/ErrorDetailsBlock.tsx b/apps/website/components/errors/ErrorDetailsBlock.tsx index 93b751086..18563e9aa 100644 --- a/apps/website/components/errors/ErrorDetailsBlock.tsx +++ b/apps/website/components/errors/ErrorDetailsBlock.tsx @@ -3,10 +3,10 @@ import { Button } from '@/ui/Button'; import { Card } from '@/ui/Card'; import { Icon } from '@/ui/Icon'; -import { Stack } from '@/ui/primitives/Stack'; import { Text } from '@/ui/Text'; -import { ChevronDown, ChevronUp, Copy } from 'lucide-react'; -import { useState } from 'react'; +import { Accordion } from '@/ui/Accordion'; +import { Copy } from 'lucide-react'; +import React, { useState } from 'react'; interface ErrorDetailsBlockProps { error: Error & { digest?: string }; @@ -19,7 +19,6 @@ interface ErrorDetailsBlockProps { * Follows "Precision Racing Minimal" theme. */ export function ErrorDetailsBlock({ error }: ErrorDetailsBlockProps) { - const [showDetails, setShowDetails] = useState(false); const [copied, setCopied] = useState(false); const copyError = async () => { @@ -41,61 +40,28 @@ export function ErrorDetailsBlock({ error }: ErrorDetailsBlockProps) { }; return ( - - setShowDetails(!showDetails)} - direction="row" - align="center" - justify="center" - gap={2} - className="transition-all" - > - - {showDetails ? : } - {showDetails ? 'Hide Technical Logs' : 'Show Technical Logs'} - - - - {showDetails && ( - - - +
+ +
+ + {error.stack || 'No stack trace available'} {error.digest && `\n\nDigest: ${error.digest}`} - +
- - - )} - +
+
+
+
); } diff --git a/apps/website/components/errors/ErrorRecoveryActions.tsx b/apps/website/components/errors/ErrorRecoveryActions.tsx index ec854004a..0ebb0dd3d 100644 --- a/apps/website/components/errors/ErrorRecoveryActions.tsx +++ b/apps/website/components/errors/ErrorRecoveryActions.tsx @@ -1,9 +1,7 @@ 'use client'; -import { Button } from '@/ui/Button'; -import { Icon } from '@/ui/Icon'; -import { Stack } from '@/ui/primitives/Stack'; -import { Home, RefreshCw } from 'lucide-react'; +import { ErrorActionButtons } from '@/ui/ErrorActionButtons'; +import React from 'react'; interface ErrorRecoveryActionsProps { onRetry: () => void; @@ -18,30 +16,9 @@ interface ErrorRecoveryActionsProps { */ export function ErrorRecoveryActions({ onRetry, onHome }: ErrorRecoveryActionsProps) { return ( - - - - + ); } diff --git a/apps/website/components/errors/ErrorScreen.tsx b/apps/website/components/errors/ErrorScreen.tsx index a13257ca9..676a15447 100644 --- a/apps/website/components/errors/ErrorScreen.tsx +++ b/apps/website/components/errors/ErrorScreen.tsx @@ -2,11 +2,12 @@ import { Card } from '@/ui/Card'; import { Glow } from '@/ui/Glow'; -import { Stack } from '@/ui/primitives/Stack'; import { Text } from '@/ui/Text'; +import { ErrorPageContainer } from '@/ui/ErrorPageContainer'; import { AppErrorBoundaryView } from './AppErrorBoundaryView'; import { ErrorDetailsBlock } from './ErrorDetailsBlock'; import { ErrorRecoveryActions } from './ErrorRecoveryActions'; +import React from 'react'; interface ErrorScreenProps { error: Error & { digest?: string }; @@ -22,54 +23,27 @@ interface ErrorScreenProps { */ export function ErrorScreen({ error, reset, onHome }: ErrorScreenProps) { return ( - + {/* Background Accents */} - - - {/* Error Message Summary */} - - + {/* Error Message Summary */} +
+ + {error.message || 'Unknown execution error'} +
- - - -
-
-
+ + + + + ); } diff --git a/apps/website/components/errors/GlobalErrorScreen.tsx b/apps/website/components/errors/GlobalErrorScreen.tsx index f909c3007..7f10a1fd4 100644 --- a/apps/website/components/errors/GlobalErrorScreen.tsx +++ b/apps/website/components/errors/GlobalErrorScreen.tsx @@ -5,9 +5,10 @@ import { Card } from '@/ui/Card'; import { Glow } from '@/ui/Glow'; import { Heading } from '@/ui/Heading'; import { Icon } from '@/ui/Icon'; -import { Stack } from '@/ui/primitives/Stack'; import { Text } from '@/ui/Text'; +import { ErrorPageContainer } from '@/ui/ErrorPageContainer'; import { AlertTriangle, Home, RefreshCw, Terminal } from 'lucide-react'; +import React from 'react'; interface GlobalErrorScreenProps { error: Error & { digest?: string }; @@ -23,88 +24,44 @@ interface GlobalErrorScreenProps { */ export function GlobalErrorScreen({ error, reset, onHome }: GlobalErrorScreenProps) { return ( - + {/* Background Accents - Subtle telemetry vibe */} - - {/* System Status Header */} - - - - - - System Fault Detected - - - - - Status: Critical +
+
+ + + System Fault Detected + +
+ + Status: Critical + +
+ +
+ {/* Fault Description */} +
+ + The application kernel encountered an unrecoverable execution error. + Telemetry has been captured for diagnostic review. - - - - {/* Fault Description */} - - - The application kernel encountered an unrecoverable execution error. - Telemetry has been captured for diagnostic review. - + +
- - + {/* Recovery Actions */} + +
- {/* Recovery Actions */} - -
-
- - {/* Footer / Metadata */} - - - GP-CORE-ERR-{error.digest?.substring(0, 8).toUpperCase() || 'UNKNOWN'} - - -
-
+ {/* Footer / Metadata */} +
+ + GP-CORE-ERR-{error.digest?.substring(0, 8).toUpperCase() || 'UNKNOWN'} + +
+ ); } @@ -115,30 +72,23 @@ export function GlobalErrorScreen({ error, reset, onHome }: GlobalErrorScreenPro */ function SystemStatusPanel({ error }: { error: Error & { digest?: string } }) { return ( - - - - - + +
+
+ + Fault Log - - +
+ {error.message || 'Unknown execution fault'} {error.digest && ( - + Digest: {error.digest} )} - +
); } @@ -150,19 +100,11 @@ function SystemStatusPanel({ error }: { error: Error & { digest?: string } }) { */ function RecoveryActions({ onRetry, onHome }: { onRetry: () => void; onHome: () => void }) { return ( - +
@@ -170,11 +112,9 @@ function RecoveryActions({ onRetry, onHome }: { onRetry: () => void; onHome: () variant="secondary" onClick={onHome} icon={} - rounded="none" - px={8} > Return to Pits - +
); } diff --git a/apps/website/components/errors/NotFoundActions.tsx b/apps/website/components/errors/NotFoundActions.tsx index 5963a30ce..b0b630cc3 100644 --- a/apps/website/components/errors/NotFoundActions.tsx +++ b/apps/website/components/errors/NotFoundActions.tsx @@ -1,8 +1,9 @@ 'use client'; import { Button } from '@/ui/Button'; -import { Stack } from '@/ui/primitives/Stack'; +import { Group } from '@/ui/Group'; import { Text } from '@/ui/Text'; +import { StatusDot } from '@/ui/StatusDot'; interface NotFoundActionsProps { primaryLabel: string; @@ -17,12 +18,11 @@ interface NotFoundActionsProps { */ export function NotFoundActions({ primaryLabel, onPrimaryClick }: NotFoundActionsProps) { return ( - + @@ -32,18 +32,13 @@ export function NotFoundActions({ primaryLabel, onPrimaryClick }: NotFoundAction size="lg" onClick={() => window.history.back()} > - - - + + + Previous Sector - + - + ); } diff --git a/apps/website/components/errors/NotFoundCallToAction.tsx b/apps/website/components/errors/NotFoundCallToAction.tsx index 66164e206..d010aa288 100644 --- a/apps/website/components/errors/NotFoundCallToAction.tsx +++ b/apps/website/components/errors/NotFoundCallToAction.tsx @@ -1,8 +1,8 @@ 'use client'; import { Button } from '@/ui/Button'; -import { Stack } from '@/ui/primitives/Stack'; import { Text } from '@/ui/Text'; +import React from 'react'; interface NotFoundCallToActionProps { label: string; @@ -17,7 +17,7 @@ interface NotFoundCallToActionProps { */ export function NotFoundCallToAction({ label, onClick }: NotFoundCallToActionProps) { return ( - +
- + Telemetry connection lost - +
); } diff --git a/apps/website/components/errors/NotFoundDiagnostics.tsx b/apps/website/components/errors/NotFoundDiagnostics.tsx index a6483ced7..4cc95c619 100644 --- a/apps/website/components/errors/NotFoundDiagnostics.tsx +++ b/apps/website/components/errors/NotFoundDiagnostics.tsx @@ -1,7 +1,8 @@ 'use client'; -import { Stack } from '@/ui/primitives/Stack'; +import { Group } from '@/ui/Group'; import { Text } from '@/ui/Text'; +import { Badge } from '@/ui/Badge'; interface NotFoundDiagnosticsProps { errorCode: string; @@ -15,35 +16,19 @@ interface NotFoundDiagnosticsProps { */ export function NotFoundDiagnostics({ errorCode }: NotFoundDiagnosticsProps) { return ( - - - - {errorCode} - - + + + {errorCode} + Telemetry connection lost // Sector data unavailable - + ); } diff --git a/apps/website/components/errors/NotFoundHelpLinks.tsx b/apps/website/components/errors/NotFoundHelpLinks.tsx index 430b0f44c..5c3dcc991 100644 --- a/apps/website/components/errors/NotFoundHelpLinks.tsx +++ b/apps/website/components/errors/NotFoundHelpLinks.tsx @@ -1,7 +1,8 @@ 'use client'; -import { Stack } from '@/ui/primitives/Stack'; +import { NavGroup } from '@/ui/NavGroup'; import { Text } from '@/ui/Text'; +import { Link } from '@/ui/Link'; import React from 'react'; interface NotFoundHelpLinksProps { @@ -16,31 +17,20 @@ interface NotFoundHelpLinksProps { */ export function NotFoundHelpLinks({ links }: NotFoundHelpLinksProps) { return ( - - {links.map((link, index) => ( - - + {links.map((link) => ( + + - - {link.label} - - - {index < links.length - 1 && ( - - )} - + {link.label} +
+ ))} -
+ ); } diff --git a/apps/website/components/errors/NotFoundScreen.tsx b/apps/website/components/errors/NotFoundScreen.tsx index 82cf8a70c..53fa20228 100644 --- a/apps/website/components/errors/NotFoundScreen.tsx +++ b/apps/website/components/errors/NotFoundScreen.tsx @@ -1,12 +1,13 @@ 'use client'; -import { Card } from '@/ui/Card'; +import { ErrorPageContainer } from '@/ui/ErrorPageContainer'; import { Glow } from '@/ui/Glow'; -import { Stack } from '@/ui/primitives/Stack'; import { Text } from '@/ui/Text'; +import { FooterSection } from '@/ui/FooterSection'; import { NotFoundActions } from './NotFoundActions'; import { NotFoundDiagnostics } from './NotFoundDiagnostics'; import { NotFoundHelpLinks } from './NotFoundHelpLinks'; +import React from 'react'; interface NotFoundScreenProps { errorCode: string; @@ -37,105 +38,44 @@ export function NotFoundScreen({ ]; return ( - + {/* Background Glow Accent */} - + + - - {/* Header Section */} - - - - - {title} - - + {title} + - {/* Visual Separator */} - - {null} - + + {message} + - {/* Message Section */} - - {message} - + - {/* Actions Section */} - - - {/* Footer Section */} - - {null} - - - -
- - {/* Subtle Edge Details */} - {null} - {null} -
+ + + + ); } diff --git a/apps/website/components/errors/RecoveryActions.tsx b/apps/website/components/errors/RecoveryActions.tsx index 0a2a81473..33d4c0dba 100644 --- a/apps/website/components/errors/RecoveryActions.tsx +++ b/apps/website/components/errors/RecoveryActions.tsx @@ -2,8 +2,8 @@ import { Button } from '@/ui/Button'; import { Icon } from '@/ui/Icon'; -import { Stack } from '@/ui/primitives/Stack'; import { Home, LifeBuoy, RefreshCw } from 'lucide-react'; +import React from 'react'; interface RecoveryActionsProps { onRetry: () => void; @@ -18,19 +18,11 @@ interface RecoveryActionsProps { */ export function RecoveryActions({ onRetry, onHome }: RecoveryActionsProps) { return ( - +
@@ -38,7 +30,6 @@ export function RecoveryActions({ onRetry, onHome }: RecoveryActionsProps) { variant="secondary" onClick={onHome} icon={} - width="160px" > Return to Pits @@ -49,10 +40,9 @@ export function RecoveryActions({ onRetry, onHome }: RecoveryActionsProps) { target="_blank" rel="noopener noreferrer" icon={} - width="160px" > Contact Support - +
); } diff --git a/apps/website/components/errors/ServerErrorPanel.tsx b/apps/website/components/errors/ServerErrorPanel.tsx index eb22eea17..ac4b89394 100644 --- a/apps/website/components/errors/ServerErrorPanel.tsx +++ b/apps/website/components/errors/ServerErrorPanel.tsx @@ -3,9 +3,9 @@ import { Card } from '@/ui/Card'; import { Heading } from '@/ui/Heading'; import { Icon } from '@/ui/Icon'; -import { Stack } from '@/ui/primitives/Stack'; import { Text } from '@/ui/Text'; import { AlertTriangle } from 'lucide-react'; +import React from 'react'; interface ServerErrorPanelProps { message?: string; @@ -20,57 +20,53 @@ interface ServerErrorPanelProps { */ export function ServerErrorPanel({ message, incidentId }: ServerErrorPanelProps) { return ( - +
{/* Status Indicator */} - - - + +
{/* Primary Message */} - +
CRITICAL_SYSTEM_FAILURE - + The application engine encountered an unrecoverable state. Telemetry has been dispatched to engineering. - +
{/* Technical Summary */} - - - - STATUS: 500_INTERNAL_SERVER_ERROR - - {message && ( - - EXCEPTION: {message} +
+ +
+ + STATUS: 500_INTERNAL_SERVER_ERROR - )} - {incidentId && ( - - INCIDENT_ID: {incidentId} - - )} - - - + {message && ( + + EXCEPTION: {message} + + )} + {incidentId && ( + + INCIDENT_ID: {incidentId} + + )} +
+
+
+ ); } diff --git a/apps/website/components/feed/ActivityFeed.tsx b/apps/website/components/feed/ActivityFeed.tsx index 9c33cb0ff..01b777747 100644 --- a/apps/website/components/feed/ActivityFeed.tsx +++ b/apps/website/components/feed/ActivityFeed.tsx @@ -1,11 +1,13 @@ +'use client'; + import React from 'react'; import { Activity } from 'lucide-react'; import { Card } from '@/ui/Card'; -import { Heading } from '@/ui/Heading'; +import { SectionHeader } from '@/ui/SectionHeader'; import { ActivityItem } from '@/ui/ActivityItem'; import { Icon } from '@/ui/Icon'; -import { ActivityFeedList } from '@/components/feed/ActivityFeedList'; -import { MinimalEmptyState } from '@/components/shared/state/EmptyState'; +import { EmptyState } from '@/ui/EmptyState'; +import { Button } from '@/ui/Button'; interface FeedItem { id: string; @@ -24,27 +26,34 @@ interface ActivityFeedProps { export function ActivityFeed({ items, hasItems }: ActivityFeedProps) { return ( - } mb={4}> - Recent Activity - + } + /> {hasItems ? ( - +
{items.slice(0, 5).map((item) => ( + title={item.headline} + description={item.body} + timestamp={item.formattedTime} + > + {item.ctaHref && item.ctaLabel && ( + + )} + ))} - +
) : ( - )}
diff --git a/apps/website/components/feed/ActivityFeedItem.tsx b/apps/website/components/feed/ActivityFeedItem.tsx index 5ebe54d2f..788753757 100644 --- a/apps/website/components/feed/ActivityFeedItem.tsx +++ b/apps/website/components/feed/ActivityFeedItem.tsx @@ -1,9 +1,7 @@ +'use client'; - -import { Box } from '@/ui/primitives/Box'; -import { Surface } from '@/ui/primitives/Surface'; -import { Text } from '@/ui/Text'; -import { ReactNode } from 'react'; +import { ActivityItem } from '@/ui/ActivityItem'; +import React, { ReactNode } from 'react'; interface ActivityFeedItemProps { icon: ReactNode; @@ -17,34 +15,13 @@ export function ActivityFeedItem({ timestamp, }: ActivityFeedItemProps) { return ( - - - {icon} - - - - {content} - - - {timestamp} - - - + {typeof content !== 'string' && content} + ); } diff --git a/apps/website/components/feed/ActivityFeedList.tsx b/apps/website/components/feed/ActivityFeedList.tsx index a27e85af7..81e1bd47a 100644 --- a/apps/website/components/feed/ActivityFeedList.tsx +++ b/apps/website/components/feed/ActivityFeedList.tsx @@ -1,5 +1,4 @@ -import { Stack } from '@/ui/primitives/Stack'; -import { ReactNode } from 'react'; +import React, { ReactNode } from 'react'; interface ActivityFeedListProps { children: ReactNode; @@ -7,8 +6,8 @@ interface ActivityFeedListProps { export function ActivityFeedList({ children }: ActivityFeedListProps) { return ( - +
{children} - +
); } diff --git a/apps/website/components/feed/FeedItemCard.tsx b/apps/website/components/feed/FeedItemCard.tsx index b4d12e117..cf47323df 100644 --- a/apps/website/components/feed/FeedItemCard.tsx +++ b/apps/website/components/feed/FeedItemCard.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react'; import { Button } from '@/ui/Button'; import { FeedItem } from '@/ui/FeedItem'; +import { TimeDisplay } from '@/lib/display-objects/TimeDisplay'; interface FeedItemData { id: string; @@ -15,18 +16,6 @@ interface FeedItemData { ctaLabel?: string; } -function timeAgo(timestamp: Date | string): string { - const date = typeof timestamp === 'string' ? new Date(timestamp) : timestamp; - const diffMs = Date.now() - date.getTime(); - const diffMinutes = Math.floor(diffMs / 60000); - if (diffMinutes < 1) return 'Just now'; - if (diffMinutes < 60) return `${diffMinutes} min ago`; - const diffHours = Math.floor(diffMinutes / 60); - if (diffHours < 24) return `${diffHours} h ago`; - const diffDays = Math.floor(diffHours / 24); - return `${diffDays} d ago`; -} - async function resolveActor() { return null; } @@ -55,20 +44,23 @@ export function FeedItemCard({ item }: FeedItemCardProps) { return ( +
{item.headline}
+ {item.body &&
{item.body}
} + + } + actions={item.ctaHref && item.ctaLabel ? ( diff --git a/apps/website/components/feed/FeedList.tsx b/apps/website/components/feed/FeedList.tsx index 97eef3726..2f163cb23 100644 --- a/apps/website/components/feed/FeedList.tsx +++ b/apps/website/components/feed/FeedList.tsx @@ -1,6 +1,6 @@ import { FeedItemCard } from '@/components/feed/FeedItemCard'; import { FeedEmptyState } from '@/ui/FeedEmptyState'; -import { Stack } from '@/ui/primitives/Stack'; +import React from 'react'; interface FeedItemData { id: string; @@ -23,10 +23,10 @@ export function FeedList({ items }: FeedListProps) { } return ( - +
{items.map(item => ( ))} - +
); } diff --git a/apps/website/components/feed/RecentActivity.tsx b/apps/website/components/feed/RecentActivity.tsx index a73b99823..972b7401b 100644 --- a/apps/website/components/feed/RecentActivity.tsx +++ b/apps/website/components/feed/RecentActivity.tsx @@ -1,28 +1,13 @@ -import { ActiveDriverCard } from '@/components/drivers/ActiveDriverCard'; +'use client'; + +import { ActiveDriverCard } from '@/ui/ActiveDriverCard'; import { mediaConfig } from '@/lib/config/mediaConfig'; -import { Heading } from '@/ui/Heading'; +import { SectionHeader } from '@/ui/SectionHeader'; import { Icon } from '@/ui/Icon'; -import { Box } from '@/ui/primitives/Box'; -import { Text } from '@/ui/Text'; import { Activity } from 'lucide-react'; - -const SKILL_LEVELS = [ - { id: 'pro', label: 'Pro', color: 'text-yellow-400' }, - { id: 'advanced', label: 'Advanced', color: 'text-purple-400' }, - { id: 'intermediate', label: 'Intermediate', color: 'text-primary-blue' }, - { id: 'beginner', label: 'Beginner', color: 'text-green-400' }, -]; - -const CATEGORIES = [ - { id: 'beginner', label: 'Beginner', color: 'text-green-400' }, - { id: 'intermediate', label: 'Intermediate', color: 'text-primary-blue' }, - { id: 'advanced', label: 'Advanced', color: 'text-purple-400' }, - { id: 'pro', label: 'Pro', color: 'text-yellow-400' }, - { id: 'endurance', label: 'Endurance', color: 'text-orange-400' }, - { id: 'sprint', label: 'Sprint', color: 'text-red-400' }, -]; +import React from 'react'; interface RecentActivityProps { drivers: { @@ -40,45 +25,28 @@ export function RecentActivity({ drivers, onDriverClick }: RecentActivityProps) const activeDrivers = drivers.filter((d) => d.isActive).slice(0, 6); return ( - - - - - - - Active Drivers - Currently competing in leagues - - +
+ } + /> - +
{activeDrivers.map((driver) => { - const levelConfig = SKILL_LEVELS.find((l) => l.id === driver.skillLevel); - const categoryConfig = CATEGORIES.find((c) => c.id === driver.category); return ( onDriverClick(driver.id)} /> ); })} - - +
+
); } diff --git a/apps/website/components/layout/NavLink.tsx b/apps/website/components/layout/NavLink.tsx deleted file mode 100644 index abdcc1092..000000000 --- a/apps/website/components/layout/NavLink.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Text } from '@/ui/Text'; -import { Box } from '@/ui/primitives/Box'; -import { LucideIcon } from 'lucide-react'; -import Link from 'next/link'; - -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 ( - - {Icon && } - - {label} - - - ); - } - - return ( - - {Icon && } - {label} - {isActive && ( - - )} - - ); -} diff --git a/apps/website/components/leaderboards/DeltaChip.tsx b/apps/website/components/leaderboards/DeltaChip.tsx index 6dc86dc77..ae24b74ae 100644 --- a/apps/website/components/leaderboards/DeltaChip.tsx +++ b/apps/website/components/leaderboards/DeltaChip.tsx @@ -1,7 +1,8 @@ import { Icon } from '@/ui/Icon'; -import { Box } from '@/ui/primitives/Box'; import { Text } from '@/ui/Text'; +import { Badge } from '@/ui/Badge'; import { ChevronDown, ChevronUp, Minus } from 'lucide-react'; +import React from 'react'; interface DeltaChipProps { value: number; @@ -11,40 +12,26 @@ interface DeltaChipProps { export function DeltaChip({ value, type = 'rank' }: DeltaChipProps) { if (value === 0) { return ( - - - 0 - +
+ + 0 +
); } const isPositive = value > 0; - const color = isPositive - ? (type === 'rank' ? 'text-performance-green' : 'text-performance-green') - : (type === 'rank' ? 'text-error-red' : 'text-error-red'); - - // For rank, positive delta usually means dropping positions (e.g. +1 rank means 1st -> 2nd) - // But usually "Delta" in leaderboards means "positions gained/lost" - // Let's assume value is "positions gained" (positive = up, negative = down) - + const variant = isPositive ? 'success' : 'critical'; const IconComponent = isPositive ? ChevronUp : ChevronDown; const absoluteValue = Math.abs(value); return ( - - - - {absoluteValue} - - + +
+ + + {absoluteValue} + +
+
); } diff --git a/apps/website/components/leaderboards/LeaderboardFiltersBar.tsx b/apps/website/components/leaderboards/LeaderboardFiltersBar.tsx index a92909513..f0d094eda 100644 --- a/apps/website/components/leaderboards/LeaderboardFiltersBar.tsx +++ b/apps/website/components/leaderboards/LeaderboardFiltersBar.tsx @@ -1,6 +1,7 @@ import { Icon } from '@/ui/Icon'; -import { Stack } from '@/ui/primitives/Stack'; -import { Text } from '@/ui/Text'; +import { Input } from '@/ui/Input'; +import { ControlBar } from '@/ui/ControlBar'; +import { Button } from '@/ui/Button'; import { Filter, Search } from 'lucide-react'; import React from 'react'; @@ -18,70 +19,32 @@ export function LeaderboardFiltersBar({ children, }: LeaderboardFiltersBarProps) { return ( - - - - - - - ) => onSearchChange?.(e.target.value)} - placeholder={placeholder} - w="full" - bg="bg-graphite-black/50" - border - borderColor="border-charcoal-outline" - rounded="md" - py={2} - pl={10} - pr={4} - fontSize="0.875rem" - color="text-white" - transition - hoverBorderColor="border-primary-blue/50" - /> - - - +
+ + onSearchChange?.(e.target.value)} + placeholder={placeholder} + icon={} + fullWidth + /> +
+ } + > +
{children} - } > - - Filters - - - - + Filters + +
+ + ); } diff --git a/apps/website/components/leaderboards/RankBadge.tsx b/apps/website/components/leaderboards/RankBadge.tsx index f63195002..7ce5f534e 100644 --- a/apps/website/components/leaderboards/RankBadge.tsx +++ b/apps/website/components/leaderboards/RankBadge.tsx @@ -1,15 +1,20 @@ - - -import { Box } from '@/ui/primitives/Box'; +import { Badge } from '@/ui/Badge'; import { Text } from '@/ui/Text'; +import React from 'react'; interface RankBadgeProps { rank: number; - size?: 'sm' | 'md' | 'lg'; - showLabel?: boolean; + size?: 'sm' | 'md'; } -export function RankBadge({ rank, size = 'md', showLabel = true }: RankBadgeProps) { +export function RankBadge({ rank, size = 'md' }: RankBadgeProps) { + const getVariant = (rank: number): 'warning' | 'primary' | 'info' | 'default' => { + if (rank <= 3) return 'warning'; + if (rank <= 10) return 'primary'; + if (rank <= 50) return 'info'; + return 'default'; + }; + const getMedalEmoji = (rank: number) => { switch (rank) { case 1: return '🥇'; @@ -21,32 +26,12 @@ export function RankBadge({ rank, size = 'md', showLabel = true }: RankBadgeProp const medal = getMedalEmoji(rank); - const sizeClasses = { - sm: 'text-sm px-2 py-1', - md: 'text-base px-3 py-1.5', - lg: 'text-lg px-4 py-2' - }; - - const getRankColor = (rank: number) => { - if (rank <= 3) return 'bg-warning-amber/20 text-warning-amber border-warning-amber/30'; - if (rank <= 10) return 'bg-primary-blue/20 text-primary-blue border-primary-blue/30'; - if (rank <= 50) return 'bg-purple-500/20 text-purple-400 border-purple-500/30'; - return 'bg-charcoal-outline/20 text-gray-300 border-charcoal-outline'; - }; - return ( - - {medal && {medal}} - {showLabel && #{rank}} - {!showLabel && !medal && #{rank}} - + +
+ {medal && {medal}} + #{rank} +
+
); } diff --git a/apps/website/components/leaderboards/RankMedal.tsx b/apps/website/components/leaderboards/RankMedal.tsx index 4e51f05e5..a04474a07 100644 --- a/apps/website/components/leaderboards/RankMedal.tsx +++ b/apps/website/components/leaderboards/RankMedal.tsx @@ -1,8 +1,8 @@ import { MedalDisplay } from '@/lib/display-objects/MedalDisplay'; import { Icon } from '@/ui/Icon'; -import { Box } from '@/ui/primitives/Box'; import { Text } from '@/ui/Text'; import { Crown, Medal } from 'lucide-react'; +import React from 'react'; interface RankMedalProps { rank: number; @@ -12,11 +12,12 @@ interface RankMedalProps { export function RankMedal({ rank, size = 'md', showIcon = true }: RankMedalProps) { const isTop3 = rank <= 3; + const variant = MedalDisplay.getVariant(rank); - const sizeMap = { - sm: '7', - md: '8', - lg: '10', + const sizePx = { + sm: '1.75rem', + md: '2rem', + lg: '2.5rem', }; const textSizeMap = { @@ -32,22 +33,23 @@ export function RankMedal({ rank, size = 'md', showIcon = true }: RankMedalProps }; return ( - {isTop3 && showIcon ? ( - + ) : ( - {rank} + {rank} )} - + ); } diff --git a/apps/website/components/leaderboards/TeamRankingRow.tsx b/apps/website/components/leaderboards/TeamRankingRow.tsx index b84171d26..b38ddd129 100644 --- a/apps/website/components/leaderboards/TeamRankingRow.tsx +++ b/apps/website/components/leaderboards/TeamRankingRow.tsx @@ -1,9 +1,9 @@ import { getMediaUrl } from '@/lib/utilities/media'; import { Image } from '@/ui/Image'; -import { Box } from '@/ui/primitives/Box'; import { TableCell, TableRow } from '@/ui/Table'; import { Text } from '@/ui/Text'; import { RankMedal } from './RankMedal'; +import React from 'react'; interface TeamRankingRowProps { id: string; @@ -32,70 +32,62 @@ export function TeamRankingRow({ - +
- +
- - +
+
- - +
+
{name} - + {memberCount} Members - - +
+
- + {rating} - + {wins} - {races} + {races}
); diff --git a/apps/website/components/leagues/EmptyState.tsx b/apps/website/components/leagues/EmptyState.tsx index f99b92235..7e8d35c0b 100644 --- a/apps/website/components/leagues/EmptyState.tsx +++ b/apps/website/components/leagues/EmptyState.tsx @@ -1,6 +1,7 @@ import { Trophy, Sparkles, LucideIcon } from 'lucide-react'; import { Card } from '@/ui/Card'; -import { EmptyState as UiEmptyState } from '@/components/shared/state/EmptyState'; +import { EmptyState as UiEmptyState } from '@/ui/EmptyState'; +import React from 'react'; interface EmptyStateProps { title: string; @@ -32,8 +33,10 @@ export function EmptyState({ onClick: onAction, icon: actionIcon, } : undefined} - /> - {children} + variant="minimal" + > + {children} +
); } diff --git a/apps/website/components/leagues/JoinRequestsPanel.tsx b/apps/website/components/leagues/JoinRequestsPanel.tsx index 537e9af4a..59c717554 100644 --- a/apps/website/components/leagues/JoinRequestsPanel.tsx +++ b/apps/website/components/leagues/JoinRequestsPanel.tsx @@ -1,9 +1,13 @@ -import { Button } from '@/ui/Button'; -import { Heading } from '@/ui/Heading'; +'use client'; + +import { IconButton } from '@/ui/IconButton'; +import { Panel } from '@/ui/Panel'; import { Icon } from '@/ui/Icon'; -import { Stack } from '@/ui/primitives/Stack'; import { Text } from '@/ui/Text'; -import { Check, Clock, X } from 'lucide-react'; +import { ListItem, ListItemInfo, ListItemActions } from '@/ui/ListItem'; +import { EmptyState } from '@/ui/EmptyState'; +import { Check, Clock, X, UserPlus } from 'lucide-react'; +import React from 'react'; interface JoinRequestsPanelProps { requests: Array<{ @@ -20,67 +24,49 @@ interface JoinRequestsPanelProps { export function JoinRequestsPanel({ requests, onAccept, onDecline }: JoinRequestsPanelProps) { if (requests.length === 0) { return ( - - No pending join requests - + ); } return ( - - - - Pending Requests ({requests.length}) - - - + +
{requests.map((request) => ( - - - - - - {request.driverName.substring(0, 2).toUpperCase()} - - - - {request.driverName} - - - {request.requestedAt} - - - - - - - - - - {request.message && ( - - - “{request.message}” - - - )} - + + + + {request.requestedAt} +
+ } + /> + + onDecline(request.id)} + icon={X} + intent="critical" + title="Decline" + /> + onAccept(request.id)} + icon={Check} + title="Accept" + /> + + ))} -
-
+ + ); } diff --git a/apps/website/components/leagues/LeagueCard.tsx b/apps/website/components/leagues/LeagueCard.tsx index 1564655cf..9674176e6 100644 --- a/apps/website/components/leagues/LeagueCard.tsx +++ b/apps/website/components/leagues/LeagueCard.tsx @@ -1,13 +1,13 @@ - +'use client'; import { Heading } from '@/ui/Heading'; import { Icon } from '@/ui/Icon'; import { Image } from '@/ui/Image'; import { PlaceholderImage } from '@/ui/PlaceholderImage'; -import { Stack } from '@/ui/primitives/Stack'; import { Text } from '@/ui/Text'; -import { Calendar as LucideCalendar, ChevronRight as LucideChevronRight } from 'lucide-react'; -import { ReactNode } from 'react'; +import { LeagueCard as UILeagueCard, LeagueCardStats, LeagueCardFooter } from '@/ui/LeagueCard'; +import { Calendar as LucideCalendar } from 'lucide-react'; +import React, { ReactNode } from 'react'; interface LeagueCardProps { name: string; @@ -42,151 +42,68 @@ export function LeagueCard({ fillPercentage, hasOpenSlots, openSlotsCount, - isTeamLeague: _isTeamLeague, - usedDriverSlots: _usedDriverSlots, - maxDrivers: _maxDrivers, timingSummary, onClick, }: LeagueCardProps) { return ( - + {logoUrl ? ( + + ) : ( + + )} + + } + badges={ + + {badges} + {championshipBadge} + + } > - {/* Card Container */} - - {/* Cover Image */} - - - {/* Gradient Overlay */} - - - {/* Badges - Top Left */} - - {badges} - +
+
+ {name} +
+ + + {description || 'No description available'} + - {/* Championship Type Badge - Top Right */} - - {championshipBadge} - + = 90 ? 'warning' : fillPercentage >= 70 ? 'primary' : 'success'} + /> - {/* Logo */} - - - {logoUrl ? ( - - ) : ( - - )} - - - + {hasOpenSlots && ( +
+
+ {openSlotsCount} OPEN +
+ )} - {/* Content */} - - {/* Title & Description */} - - - - {name} - - - - {description || 'No description available'} - - - {/* Stats Row */} - - {/* Primary Slots (Drivers/Teams/Nations) */} - - - {slotLabel} - - {usedSlots}/{maxSlots || '∞'} - - - - = 90 - ? 'warning-amber' - : fillPercentage >= 70 - ? 'primary-accent' - : 'success-green' - } - style={{ width: `${Math.min(fillPercentage, 100)}%` }} - /> - - - - {/* Open Slots Badge */} - {hasOpenSlots && ( - - - - {openSlotsCount} OPEN - - - )} - - - {/* Spacer to push footer to bottom */} - - - {/* Footer Info */} - - - {timingSummary && ( - - - - {timingSummary.split('•')[1]?.trim() || timingSummary} - - - )} - - - {/* View Arrow */} - - VIEW - - - - - - + + {timingSummary && ( +
+ + + {timingSummary.split('•')[1]?.trim() || timingSummary} + +
+ )} +
+ ); } - diff --git a/apps/website/components/leagues/LeagueDropSection.tsx b/apps/website/components/leagues/LeagueDropSection.tsx index ca679ebfe..86fc2c594 100644 --- a/apps/website/components/leagues/LeagueDropSection.tsx +++ b/apps/website/components/leagues/LeagueDropSection.tsx @@ -3,237 +3,13 @@ import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel'; import { Heading } from '@/ui/Heading'; import { Icon } from '@/ui/Icon'; -import { Stack } from '@/ui/primitives/Stack'; import { Text } from '@/ui/Text'; -import { Check, HelpCircle, TrendingDown, X, Zap } from 'lucide-react'; -import React, { useEffect, useRef, useState } from 'react'; -import { createPortal } from 'react-dom'; - -// ============================================================================ -// INFO FLYOUT (duplicated for self-contained component) -// ============================================================================ - -interface InfoFlyoutProps { - isOpen: boolean; - onClose: () => void; - title: string; - children: React.ReactNode; - anchorRef: React.RefObject; -} - -function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutProps) { - const [position, setPosition] = useState({ top: 0, left: 0 }); - const [mounted, setMounted] = useState(false); - const flyoutRef = useRef(null); - - useEffect(() => { - setMounted(true); - }, []); - - useEffect(() => { - if (isOpen && anchorRef.current && mounted) { - const rect = anchorRef.current.getBoundingClientRect(); - const flyoutWidth = Math.min(380, window.innerWidth - 40); - const flyoutHeight = 450; - const padding = 16; - - let left = rect.right + 12; - let top = rect.top; - - if (left + flyoutWidth > window.innerWidth - padding) { - left = rect.left - flyoutWidth - 12; - } - if (left < padding) { - left = Math.max(padding, (window.innerWidth - flyoutWidth) / 2); - } - - top = rect.top - flyoutHeight / 3; - if (top + flyoutHeight > window.innerHeight - padding) { - top = window.innerHeight - flyoutHeight - padding; - } - if (top < padding) top = padding; - - left = Math.max(padding, Math.min(left, window.innerWidth - flyoutWidth - padding)); - - setPosition({ top, left }); - } - }, [isOpen, anchorRef, mounted]); - - useEffect(() => { - const handleEscape = (e: KeyboardEvent) => { - if (e.key === 'Escape') onClose(); - }; - const handleClickOutside = (e: MouseEvent) => { - if (flyoutRef.current && !flyoutRef.current.contains(e.target as Node)) { - onClose(); - } - }; - - if (isOpen) { - document.addEventListener('keydown', handleEscape); - document.addEventListener('mousedown', handleClickOutside); - } - return () => { - document.removeEventListener('keydown', handleEscape); - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [isOpen, onClose]); - - if (!isOpen) return null; - - return createPortal( - - - - - {title} - - - - - - - {children} - - , - document.body - ); -} - -function InfoButton({ onClick, buttonRef }: { onClick: () => void; buttonRef: React.Ref }) { - return ( - - - - ); -} - -// Drop Rules Mockup -function DropRulesMockup() { - const results = [ - { round: 'R1', pts: 25, dropped: false }, - { round: 'R2', pts: 18, dropped: false }, - { round: 'R3', pts: 4, dropped: true }, - { round: 'R4', pts: 15, dropped: false }, - { round: 'R5', pts: 12, dropped: false }, - { round: 'R6', pts: 0, dropped: true }, - ]; - - const total = results.filter(r => !r.dropped).reduce((sum, r) => sum + r.pts, 0); - const wouldBe = results.reduce((sum, r) => sum + r.pts, 0); - - return ( - - - Best 4 of 6 Results - - - {results.map((r, i) => ( - - - {r.round} - - - {r.pts} - - - ))} - - - Total counted: - {total} pts - - - - Without drops: - - - {wouldBe} pts - - - - ); -} +import { InfoFlyout } from '@/ui/InfoFlyout'; +import { Stepper } from '@/ui/Stepper'; +import { Button } from '@/ui/Button'; +import { IconButton } from '@/ui/IconButton'; +import { Check, HelpCircle, TrendingDown, Zap } from 'lucide-react'; +import React, { useRef, useState } from 'react'; interface LeagueDropSectionProps { form: LeagueConfigFormModel; @@ -243,43 +19,6 @@ interface LeagueDropSectionProps { type DropStrategy = 'none' | 'bestNResults' | 'dropWorstN'; -// Drop rule info content -const DROP_RULE_INFO: Record = { - none: { - title: 'All Results Count', - description: 'Every race result affects the championship standings with no exceptions.', - details: [ - 'All race results count toward final standings', - 'No safety net for bad races or DNFs', - 'Rewards consistency across entire season', - 'Best for shorter seasons (4-6 races)', - ], - example: '8 races × your points = your total', - }, - bestNResults: { - title: 'Best N Results', - description: 'Only your top N race results count toward the championship.', - details: [ - 'Choose how many of your best races count', - 'Extra races become "bonus" opportunities', - 'Protects against occasional bad days', - 'Encourages trying even when behind', - ], - example: 'Best 6 of 8 races count', - }, - dropWorstN: { - title: 'Drop Worst N Results', - description: 'Your N worst race results are excluded from championship calculations.', - details: [ - 'Automatically removes your worst performances', - 'Great for handling DNFs or incidents', - 'All other races count normally', - 'Common in real-world championships', - ], - example: 'Drop 2 worst → 6 of 8 count', - }, -}; - const DROP_OPTIONS: Array<{ value: DropStrategy; label: string; @@ -317,13 +56,7 @@ export function LeagueDropSection({ const disabled = readOnly || !onChange; const dropPolicy = form.dropPolicy || { strategy: 'none' as const }; const [showDropFlyout, setShowDropFlyout] = useState(false); - const [activeDropRuleFlyout, setActiveDropRuleFlyout] = useState(null); const dropInfoRef = useRef(null!); - const dropRuleRefs = useRef>({ - none: null, - bestNResults: null, - dropWorstN: null, - }); const handleStrategyChange = (strategy: DropStrategy) => { if (disabled || !onChange) return; @@ -344,10 +77,8 @@ export function LeagueDropSection({ onChange(next); }; - const handleNChange = (delta: number) => { + const handleNChange = (newValue: number) => { if (disabled || !onChange || dropPolicy.strategy === 'none') return; - const current = dropPolicy.n ?? 1; - const newValue = Math.max(1, current + delta); onChange({ ...form, dropPolicy: { @@ -360,328 +91,79 @@ export function LeagueDropSection({ const needsN = dropPolicy.strategy !== 'none'; return ( - +
{/* Section header */} - - - - - - +
+
+ +
+
+
Drop Rules - setShowDropFlyout(true)} /> - - Protect from bad races - - + setShowDropFlyout(true)} + title="Help" + /> +
+ Protect from bad races +
+
- {/* Drop Rules Flyout */} setShowDropFlyout(false)} title="Drop Rules Explained" anchorRef={dropInfoRef} > - - - Drop rules allow drivers to exclude their worst results from championship calculations. - This protects against mechanical failures, bad luck, or occasional poor performances. - - - - - Visual Example - - - - - - - Drop Strategies - - - - - - - All Count - - - Every race affects standings. Best for short seasons. - - - - - 🏆 - - - Best N Results - - - Only your top N races count. Extra races are optional. - - - - - 🗑️ - - - Drop Worst N - - - Exclude your N worst results. Forgives bad days. - - - - - - - - - - - - Pro tip: For an 8-round season, - "Best 6" or "Drop 2" are popular choices. - - - - - + + Drop rules allow drivers to exclude their worst results from championship calculations. + This protects against mechanical failures, bad luck, or occasional poor performances. + {/* Strategy buttons + N stepper inline */} - +
{DROP_OPTIONS.map((option) => { const isSelected = dropPolicy.strategy === option.value; - const ruleInfo = DROP_RULE_INFO[option.value]; return ( - - handleStrategyChange(option.value)} - display="flex" - alignItems="center" - gap={2} - px={3} - py={2} - rounded="lg" - border - borderWidth="2px" - transition - borderColor={isSelected ? 'border-primary-blue' : 'border-charcoal-outline/40'} - bg={isSelected ? 'bg-primary-blue/10' : 'bg-iron-gray/20'} - hoverBorderColor={!isSelected && !disabled ? 'border-charcoal-outline' : undefined} - hoverBg={!isSelected && !disabled ? 'bg-iron-gray/30' : undefined} - cursor={disabled ? 'default' : 'pointer'} - opacity={disabled ? 0.6 : 1} - // eslint-disable-next-line gridpilot-rules/component-classification - style={{ borderRightWidth: 0, borderTopRightRadius: 0, borderBottomRightRadius: 0 }} - > - {/* Radio indicator */} - - {isSelected && } - - - {option.emoji} - - {option.label} - - - - {/* Info button - separate from main button */} - { dropRuleRefs.current[option.value] = el; }} - type="button" - onClick={(e: React.MouseEvent) => { - e.stopPropagation(); - setActiveDropRuleFlyout(activeDropRuleFlyout === option.value ? null : option.value); - }} - display="flex" - alignItems="center" - justifyContent="center" - px={2} - py={2} - rounded="lg" - border - borderWidth="2px" - transition - borderColor={isSelected ? 'border-primary-blue' : 'border-charcoal-outline/40'} - bg={isSelected ? 'bg-primary-blue/10' : 'bg-iron-gray/20'} - hoverBorderColor={!isSelected && !disabled ? 'border-charcoal-outline' : undefined} - hoverBg={!isSelected && !disabled ? 'bg-iron-gray/30' : undefined} - cursor={disabled ? 'default' : 'pointer'} - opacity={disabled ? 0.6 : 1} - // eslint-disable-next-line gridpilot-rules/component-classification - style={{ borderLeftWidth: 0, borderTopLeftRadius: 0, borderBottomLeftRadius: 0, height: '100%' }} - > - - - - {/* Drop Rule Info Flyout */} - setActiveDropRuleFlyout(null)} - title={ruleInfo.title} - anchorRef={{ current: (dropRuleRefs.current[option.value] as HTMLElement | null) ?? dropInfoRef.current }} - > - - {ruleInfo.description} - - - - How It Works - - - {ruleInfo.details.map((detail, idx) => ( - - - {detail} - - ))} - - - - - - {option.emoji} - - - Example - - {ruleInfo.example} - - - - - - + ); })} - {/* N Stepper - only show when needed */} {needsN && ( - - N = - handleNChange(-1)} - display="flex" - h="7" - w="7" - alignItems="center" - justifyContent="center" - rounded="md" - bg="bg-iron-gray" - border - borderColor="border-charcoal-outline" - color="text-gray-400" - transition - hoverTextColor={!disabled ? 'text-white' : undefined} - hoverBorderColor={!disabled ? 'border-primary-blue' : undefined} - opacity={disabled || (dropPolicy.n ?? 1) <= 1 ? 0.4 : 1} - > - − - - - {dropPolicy.n ?? 1} - - + handleNChange(1)} - display="flex" - h="7" - w="7" - alignItems="center" - justifyContent="center" - rounded="md" - bg="bg-iron-gray" - border - borderColor="border-charcoal-outline" - color="text-gray-400" - transition - hoverTextColor={!disabled ? 'text-white' : undefined} - hoverBorderColor={!disabled ? 'border-primary-blue' : undefined} - opacity={disabled ? 0.4 : 1} - > - + - - + /> +
)} -
+
{/* Explanation text */} - + {dropPolicy.strategy === 'none' && 'Every race result affects the championship standings.'} {dropPolicy.strategy === 'bestNResults' && `Only your best ${dropPolicy.n ?? 1} results will count.`} {dropPolicy.strategy === 'dropWorstN' && `Your worst ${dropPolicy.n ?? 1} results will be excluded.`} -
+
); } diff --git a/apps/website/components/leagues/LeagueListItem.tsx b/apps/website/components/leagues/LeagueListItem.tsx index 0e825403b..eb06d7d3c 100644 --- a/apps/website/components/leagues/LeagueListItem.tsx +++ b/apps/website/components/leagues/LeagueListItem.tsx @@ -1,8 +1,10 @@ +'use client'; + import { Button } from '@/ui/Button'; -import { Card } from '@/ui/Card'; import { Link } from '@/ui/Link'; import { Text } from '@/ui/Text'; -import { Stack } from '@/ui/primitives/Stack'; +import { ListItem, ListItemInfo, ListItemActions } from '@/ui/ListItem'; +import React from 'react'; interface League { leagueId: string; @@ -18,40 +20,34 @@ interface LeagueListItemProps { export function LeagueListItem({ league, isAdmin }: LeagueListItemProps) { return ( - - - - {league.name} - - {league.description} - - {league.membershipRole && ( - + + Your role:{' '} - {league.membershipRole} + {league.membershipRole} - )} - - - - View + ) + } + /> + + + View + + {isAdmin && ( + + - {isAdmin && ( - - - - )} - - - + )} + + ); } diff --git a/apps/website/components/leagues/LeagueMemberRow.tsx b/apps/website/components/leagues/LeagueMemberRow.tsx index 2ea0dc376..3e0fbc2d6 100644 --- a/apps/website/components/leagues/LeagueMemberRow.tsx +++ b/apps/website/components/leagues/LeagueMemberRow.tsx @@ -1,10 +1,10 @@ import { DriverIdentity } from '@/components/drivers/DriverIdentity'; import { DriverViewModel } from '@/lib/view-models/DriverViewModel'; import { Badge } from '@/ui/Badge'; -import { Box } from '@/ui/primitives/Box'; import { TableCell, TableRow } from '@/ui/Table'; import { Text } from '@/ui/Text'; -import { ReactNode } from 'react'; +import { DateDisplay } from '@/lib/display-objects/DateDisplay'; +import React, { ReactNode } from 'react'; interface LeagueMemberRowProps { driver?: DriverViewModel; @@ -41,7 +41,7 @@ export function LeagueMemberRow({ return ( - +
{driver ? ( ) : ( - Unknown Driver + Unknown Driver )} {isCurrentUser && ( - (You) + (You) )} {isTopPerformer && ( )} - +
- + {rating || '—'} - + #{rank || '—'} - + {wins || 0} @@ -82,12 +82,8 @@ export function LeagueMemberRow({ - - {new Date(joinedAt).toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - })} + + {DateDisplay.formatShort(joinedAt)} {actions && ( diff --git a/apps/website/components/leagues/LeagueMembers.tsx b/apps/website/components/leagues/LeagueMembers.tsx index c4bb37179..0ed41bf64 100644 --- a/apps/website/components/leagues/LeagueMembers.tsx +++ b/apps/website/components/leagues/LeagueMembers.tsx @@ -2,7 +2,8 @@ import { LeagueMemberRow } from '@/components/leagues/LeagueMemberRow'; import { LeagueMemberTable } from '@/components/leagues/LeagueMemberTable'; -import { MinimalEmptyState } from '@/components/shared/state/EmptyState'; +import { EmptyState } from '@/ui/EmptyState'; +import { LoadingWrapper } from '@/ui/LoadingWrapper'; import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useInject } from '@/lib/di/hooks/useInject'; import { DRIVER_SERVICE_TOKEN, LEAGUE_MEMBERSHIP_SERVICE_TOKEN } from '@/lib/di/tokens'; @@ -11,10 +12,11 @@ import type { LeagueMembership } from '@/lib/types/LeagueMembership'; import type { MembershipRole } from '@/lib/types/MembershipRole'; import { DriverViewModel } from '@/lib/view-models/DriverViewModel'; import { Button } from '@/ui/Button'; -import { Box } from '@/ui/primitives/Box'; import { Select } from '@/ui/Select'; import { Text } from '@/ui/Text'; +import { ControlBar } from '@/ui/ControlBar'; import { useCallback, useEffect, useState } from 'react'; +import React from 'react'; interface LeagueMembersProps { leagueId: string; @@ -83,7 +85,6 @@ export function LeagueMembers({ return null; }; - // eslint-disable-next-line gridpilot-rules/component-no-data-manipulation const sortedMembers = [...members].sort((a, b) => { switch (sortBy) { case 'role': @@ -120,31 +121,30 @@ export function LeagueMembers({ }; if (loading) { - return ( - - Loading members... - - ); + return ; } if (members.length === 0) { return ( - ); } return ( - - {/* Sort Controls */} - - - {members.length} {members.length === 1 ? 'member' : 'members'} - - - Sort by: +
+ + {members.length} {members.length === 1 ? 'member' : 'members'} + + } + > +
+ Sort by: )} {onRemoveMember && ( @@ -211,18 +208,17 @@ export function LeagueMembers({ variant="ghost" onClick={() => onRemoveMember(member.driverId)} size="sm" - color="text-error-red" > - Remove + Remove )} - - ) : (showActions && cannotModify ? : undefined)} +
+ ) : (showActions && cannotModify ? : undefined)} /> ); })} - - +
+
); } diff --git a/apps/website/components/leagues/SponsorshipSlotCard.tsx b/apps/website/components/leagues/SponsorshipSlotCard.tsx index 05c1b47b9..a1b5c9b3f 100644 --- a/apps/website/components/leagues/SponsorshipSlotCard.tsx +++ b/apps/website/components/leagues/SponsorshipSlotCard.tsx @@ -1,12 +1,7 @@ 'use client'; -import { Badge } from '@/ui/Badge'; -import { Heading } from '@/ui/Heading'; -import { Icon } from '@/ui/Icon'; -import { Stack } from '@/ui/primitives/Stack'; -import { Surface } from '@/ui/primitives/Surface'; -import { Text } from '@/ui/Text'; -import { DollarSign } from 'lucide-react'; +import { SponsorshipCard } from '@/ui/SponsorshipCard'; +import React from 'react'; interface SponsorshipSlot { id: string; @@ -26,42 +21,12 @@ interface SponsorshipSlotCardProps { export function SponsorshipSlotCard({ slot }: SponsorshipSlotCardProps) { return ( - - - - {slot.name} - - {slot.isAvailable ? 'Available' : 'Taken'} - - - - {slot.description} - - - - - {slot.price} {slot.currency} - - - - {!slot.isAvailable && slot.sponsoredBy && ( - // eslint-disable-next-line gridpilot-rules/component-classification - - Sponsored by - {slot.sponsoredBy.name} - - )} - - + ); } diff --git a/apps/website/components/leagues/StandingsTableShell.tsx b/apps/website/components/leagues/StandingsTableShell.tsx index f950da0bc..fc159a7ed 100644 --- a/apps/website/components/leagues/StandingsTableShell.tsx +++ b/apps/website/components/leagues/StandingsTableShell.tsx @@ -1,11 +1,12 @@ 'use client'; import { Icon } from '@/ui/Icon'; -import { Stack } from '@/ui/primitives/Stack'; -import { Surface } from '@/ui/primitives/Surface'; +import { Panel } from '@/ui/Panel'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/ui/Table'; import { Text } from '@/ui/Text'; +import { PositionBadge } from '@/ui/ResultRow'; import { TrendingUp, Trophy } from 'lucide-react'; +import React from 'react'; interface StandingsEntry { position: number; @@ -23,94 +24,63 @@ interface StandingsTableShellProps { export function StandingsTableShell({ standings, title = 'Championship Standings' }: StandingsTableShellProps) { return ( - - - - - - - {title.toUpperCase()} - - - - {standings.length} Drivers - - - - -
- - - Pos - Driver - Wins - Podiums - Points - - - - {standings.map((entry) => ( - - - - - - - {entry.driverName} - {entry.change !== undefined && entry.change !== 0 && ( - - 0 ? 'text-performance-green' : 'text-error-red'} - transform={entry.change < 0 ? 'rotate(180deg)' : undefined} - /> - 0 ? 'text-performance-green' : 'text-error-red'}> - {Math.abs(entry.change)} - - - )} - - - - 0 ? 'text-white' : 'text-gray-500'}>{entry.wins} - - - 0 ? 'text-white' : 'text-gray-500'}>{entry.podiums} - - - {entry.points} - - - ))} - -
- - ); -} - -function PositionBadge({ position }: { position: number }) { - const isPodium = position <= 3; - const colors = { - 1: 'text-warning-amber bg-warning-amber/10 border-warning-amber/20', - 2: 'text-gray-300 bg-gray-300/10 border-gray-300/20', - 3: 'text-orange-400 bg-orange-400/10 border-orange-400/20', - }; - - return ( - {standings.length} Drivers + } > - - {position} - - +
+ + + + Pos + Driver + Wins + Podiums + Points + + + + {standings.map((entry) => ( + + + + + +
+ {entry.driverName} + {entry.change !== undefined && entry.change !== 0 && ( +
+ 0 ? 'success' : 'critical'} + style={{ transform: entry.change < 0 ? 'rotate(180deg)' : undefined }} + /> + 0 ? 'success' : 'critical'}> + {Math.abs(entry.change)} + +
+ )} +
+
+ + 0 ? 'high' : 'low'}>{entry.wins} + + + 0 ? 'high' : 'low'}>{entry.podiums} + + + {entry.points} + +
+ ))} +
+
+
+ ); } diff --git a/apps/website/components/media/MediaFiltersBar.tsx b/apps/website/components/media/MediaFiltersBar.tsx index d472b86f2..8557e5800 100644 --- a/apps/website/components/media/MediaFiltersBar.tsx +++ b/apps/website/components/media/MediaFiltersBar.tsx @@ -1,8 +1,13 @@ +'use client'; + import { IconButton } from '@/ui/IconButton'; import { Input } from '@/ui/Input'; -import { Stack } from '@/ui/primitives/Stack'; import { Select } from '@/ui/Select'; +import { ControlBar } from '@/ui/ControlBar'; +import { SegmentedControl } from '@/ui/SegmentedControl'; +import { Icon } from '@/ui/Icon'; import { Grid, List, Search } from 'lucide-react'; +import React from 'react'; export interface MediaFiltersBarProps { searchQuery: string; @@ -24,56 +29,41 @@ export function MediaFiltersBar({ onViewModeChange, }: MediaFiltersBarProps) { return ( - - - onSearchChange(e.target.value)} - icon={} - /> - - - - - onSearchChange(e.target.value)} + icon={} + fullWidth /> - onViewModeChange('list')} - color={viewMode === 'list' ? 'text-white' : 'text-gray-400'} - backgroundColor={viewMode === 'list' ? 'bg-blue-600' : undefined} +
+ } + > +
+
+ onUpdate?.({ favoriteCarClass: e.target.value })} - options={[ - { value: 'GT3', label: 'GT3' }, - { value: 'GT4', label: 'GT4' }, - { value: 'Formula', label: 'Formula' }, - { value: 'LMP2', label: 'LMP2' }, - ]} - /> - onUpdate?.({ favoriteCarClass: e.target.value })} + options={[ + { value: 'GT3', label: 'GT3' }, + { value: 'GT4', label: 'GT4' }, + { value: 'Formula', label: 'Formula' }, + { value: 'LMP2', label: 'LMP2' }, + ]} + /> + onUpdate?.({ country: e.target.value })} - placeholder="e.g. US, GB, DE" - maxLength={2} - /> -