diff --git a/apps/website/components/admin/AdminDashboardWrapper.tsx b/apps/website/app/admin/AdminDashboardWrapper.tsx similarity index 100% rename from apps/website/components/admin/AdminDashboardWrapper.tsx rename to apps/website/app/admin/AdminDashboardWrapper.tsx diff --git a/apps/website/components/admin/AdminUsersWrapper.tsx b/apps/website/app/admin/users/AdminUsersWrapper.tsx similarity index 100% rename from apps/website/components/admin/AdminUsersWrapper.tsx rename to apps/website/app/admin/users/AdminUsersWrapper.tsx diff --git a/apps/website/app/drivers/DriversPageClient.tsx b/apps/website/app/drivers/DriversPageClient.tsx index 9954132bf..7c89f7d79 100644 --- a/apps/website/app/drivers/DriversPageClient.tsx +++ b/apps/website/app/drivers/DriversPageClient.tsx @@ -1,36 +1,50 @@ 'use client'; import React from 'react'; -import { useRouter } from 'next/navigation'; import { DriversTemplate } from '@/templates/DriversTemplate'; -import { useDriverSearch } from '@/lib/hooks/useDriverSearch'; -import type { DriverLeaderboardViewModel } from '@/lib/view-data/DriverLeaderboardViewModel'; +import type { DriverLeaderboardViewModel } from '@/lib/view-models/DriverLeaderboardViewModel'; interface DriversPageClientProps { - data: DriverLeaderboardViewModel | null; + pageDto: DriverLeaderboardViewModel | null; + error?: string; + empty?: { + title: string; + description: string; + }; } -export function DriversPageClient({ data }: DriversPageClientProps) { - const router = useRouter(); - const drivers = data?.drivers || []; - const { searchQuery, setSearchQuery, filteredDrivers } = useDriverSearch(drivers); +/** + * DriversPageClient + * + * Client component that: + * 1. Passes ViewModel directly to Template + * + * No business logic, filtering, or sorting here. + * All data transformation happens in the PageQuery and ViewModelBuilder. + */ +export function DriversPageClient({ pageDto, error, empty }: DriversPageClientProps) { + // Handle error/empty states + if (error) { + return ( +
+
Error loading drivers
+

Please try again later

+
+ ); + } - const handleDriverClick = (driverId: string) => { - router.push(`/drivers/${driverId}`); - }; + if (!pageDto || pageDto.drivers.length === 0) { + if (empty) { + return ( +
+

{empty.title}

+

{empty.description}

+
+ ); + } + return null; + } - const handleViewLeaderboard = () => { - router.push('/leaderboards/drivers'); - }; - - return ( - - ); -} + // Pass ViewModel directly to template + return ; +} \ No newline at end of file diff --git a/apps/website/components/drivers/DriverProfilePageClient.tsx b/apps/website/app/drivers/[id]/DriverProfilePageClient.tsx similarity index 100% rename from apps/website/components/drivers/DriverProfilePageClient.tsx rename to apps/website/app/drivers/[id]/DriverProfilePageClient.tsx diff --git a/apps/website/components/leagues/LeaguesClient.tsx b/apps/website/app/leagues/LeaguesClient.tsx similarity index 79% rename from apps/website/components/leagues/LeaguesClient.tsx rename to apps/website/app/leagues/LeaguesClient.tsx index 03a5b75ca..6f1c40b77 100644 --- a/apps/website/components/leagues/LeaguesClient.tsx +++ b/apps/website/app/leagues/LeaguesClient.tsx @@ -19,10 +19,17 @@ import { Timer, } from 'lucide-react'; import LeagueCard from '@/components/leagues/LeagueCard'; -import Button from '@/ui/Button'; -import Card from '@/ui/Card'; -import Input from '@/ui/Input'; -import Heading from '@/ui/Heading'; +import { Button } from '@/ui/Button'; +import { Card } from '@/ui/Card'; +import { Input } from '@/ui/Input'; +import { Heading } from '@/ui/Heading'; +import { Container } from '@/ui/Container'; +import { Box } from '@/ui/Box'; +import { Stack } from '@/ui/Stack'; +import { Text } from '@/ui/Text'; +import { Grid } from '@/ui/Grid'; +import { GridItem } from '@/ui/GridItem'; +import { HeroSection } from '@/components/shared/HeroSection'; import type { LeaguesViewData } from '@/lib/view-data/LeaguesViewData'; import type { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel'; @@ -459,91 +466,61 @@ export function LeaguesClient({ ]; return ( -
+ {/* Hero Section */} -
- {/* Background decoration */} -
-
- -
-
-
-
- -
- - Find Your Grid - -
-

- From casual sprints to epic endurance battles — discover the perfect league for your racing style. -

- - {/* Stats */} -
-
-
- - {viewData.leagues.length} active leagues - -
-
-
- - {leaguesByCategory.new.length} new this week - -
-
-
- - {leaguesByCategory.openSlots.length} with open slots - -
-
-
- - {/* CTA */} -
- - - Create League - -

Set up your own racing series

-
-
-
+ { window.location.href = '/leagues/create'; }, + icon: Plus, + description: 'Set up your own racing series' + } + ]} + /> {/* Search and Filter Bar */} -
-
+ + {/* Search */} -
- + + + + setSearchQuery(e.target.value)} + onChange={(e: React.ChangeEvent) => setSearchQuery(e.target.value)} className="pl-11" /> -
+
{/* Filter toggle (mobile) */} - -
+ + + + {/* Category Tabs */} -
-
+ + {CATEGORIES.map((category) => { const Icon = category.icon; const count = leaguesByCategory[category.id].length; @@ -570,33 +547,36 @@ export function LeaguesClient({ ); })} -
-
-
+ + + {/* Content */} {viewData.leagues.length === 0 ? ( /* Empty State */ - + + ) : activeCategory === 'all' && !searchQuery ? ( /* Slider View - Show featured categories with sliders at different speeds and directions */ -
+ {featuredCategoriesWithSpeed .map(({ id, speed, direction }) => { const category = CATEGORIES.find((c) => c.id === id)!; @@ -616,25 +596,25 @@ export function LeaguesClient({ scrollDirection={direction} /> ))} -
+ ) : ( /* Grid View - Filtered by category or search */ -
+ {categoryFilteredLeagues.length > 0 ? ( <> -
-

- Showing {categoryFilteredLeagues.length}{' '} + + + Showing {categoryFilteredLeagues.length}{' '} {categoryFilteredLeagues.length === 1 ? 'league' : 'leagues'} {searchQuery && ( {' '} - for "{searchQuery}" + for "{searchQuery}" )} -

-
-
+ + + {categoryFilteredLeagues.map((league) => { // Convert ViewData to ViewModel for LeagueCard const viewModel: LeagueSummaryViewModel = { @@ -658,20 +638,22 @@ export function LeaguesClient({ }; return ( - - - + + + + + ); })} -
+ ) : ( -
+ -

+ No leagues found{searchQuery ? ` matching "${searchQuery}"` : ' in this category'} -

+ -
+
)} -
+ )} -
+ ); } \ No newline at end of file diff --git a/apps/website/app/leagues/[id]/stewarding/StewardingTemplate.tsx b/apps/website/app/leagues/[id]/stewarding/StewardingTemplate.tsx index cacfac8ba..3459a183f 100644 --- a/apps/website/app/leagues/[id]/stewarding/StewardingTemplate.tsx +++ b/apps/website/app/leagues/[id]/stewarding/StewardingTemplate.tsx @@ -4,9 +4,9 @@ import PenaltyFAB from '@/components/leagues/PenaltyFAB'; import QuickPenaltyModal from '@/components/leagues/QuickPenaltyModal'; import { ReviewProtestModal } from '@/components/leagues/ReviewProtestModal'; import StewardingStats from '@/components/leagues/StewardingStats'; -import Button from '@/ui/Button'; -import Card from '@/ui/Card'; -import { useLeagueStewardingMutations } from "@/lib/hooks/league/useLeagueStewardingMutations"; +import { Button } from '@/ui/Button'; +import { Card } from '@/ui/Card'; +import { useLeagueStewardingMutations } from "@/hooks/league/useLeagueStewardingMutations"; import { AlertCircle, AlertTriangle, @@ -19,13 +19,15 @@ import { } from 'lucide-react'; import Link from 'next/link'; import { useMemo, useState } from 'react'; +import { PendingProtestsList } from '@/components/leagues/PendingProtestsList'; +import { PenaltyHistoryList } from '@/components/leagues/PenaltyHistoryList'; interface StewardingData { totalPending: number; totalResolved: number; totalPenalties: number; racesWithData: Array<{ - race: { id: string; track: string; scheduledAt: Date }; + race: { id: string; track: string; scheduledAt: Date; car?: string }; pendingProtests: any[]; resolvedProtests: any[]; penalties: any[]; @@ -44,16 +46,27 @@ interface StewardingTemplateProps { export function StewardingTemplate({ data, leagueId, currentDriverId, onRefetch }: StewardingTemplateProps) { const [activeTab, setActiveTab] = useState<'pending' | 'history'>('pending'); const [selectedProtest, setSelectedProtest] = useState(null); - const [expandedRaces, setExpandedRaces] = useState>(new Set()); const [showQuickPenaltyModal, setShowQuickPenaltyModal] = useState(false); // Mutations using domain hook const { acceptProtestMutation, rejectProtestMutation } = useLeagueStewardingMutations(onRefetch); - // Filter races based on active tab - const filteredRaces = useMemo(() => { - return activeTab === 'pending' ? data.racesWithData.filter(r => r.pendingProtests.length > 0) : data.racesWithData.filter(r => r.resolvedProtests.length > 0 || r.penalties.length > 0); - }, [data, activeTab]); + // Flatten protests for the specialized list components + const allPendingProtests = useMemo(() => { + return data.racesWithData.flatMap(r => r.pendingProtests); + }, [data]); + + const allResolvedProtests = useMemo(() => { + return data.racesWithData.flatMap(r => r.resolvedProtests); + }, [data]); + + const racesMap = useMemo(() => { + const map: Record = {}; + data.racesWithData.forEach(r => { + map[r.race.id] = r.race; + }); + return map; + }, [data]); const handleAcceptProtest = async ( protestId: string, @@ -89,34 +102,6 @@ export function StewardingTemplate({ data, leagueId, currentDriverId, onRefetch }); }; - const toggleRaceExpanded = (raceId: string) => { - setExpandedRaces(prev => { - const next = new Set(prev); - if (next.has(raceId)) { - next.delete(raceId); - } else { - next.add(raceId); - } - return next; - }); - }; - - const getStatusBadge = (status: string) => { - switch (status) { - case 'pending': - case 'under_review': - return Pending; - case 'upheld': - return Upheld; - case 'dismissed': - return Dismissed; - case 'withdrawn': - return Withdrawn; - default: - return null; - } - }; - return (
@@ -168,168 +153,21 @@ export function StewardingTemplate({ data, leagueId, currentDriverId, onRefetch
{/* Content */} - {filteredRaces.length === 0 ? ( -
-
- -
-

- {activeTab === 'pending' ? 'All Clear!' : 'No History Yet'} -

-

- {activeTab === 'pending' - ? 'No pending protests to review' - : 'No resolved protests or penalties'} -

-
+ {activeTab === 'pending' ? ( + ) : ( -
- {filteredRaces.map(({ race, pendingProtests, resolvedProtests, penalties }) => { - const isExpanded = expandedRaces.has(race.id); - const displayProtests = activeTab === 'pending' ? pendingProtests : resolvedProtests; - - return ( -
- {/* Race Header */} - - - {/* Expanded Content */} - {isExpanded && ( -
- {displayProtests.length === 0 && penalties.length === 0 ? ( -

No items to display

- ) : ( - <> - {displayProtests.map((protest) => { - const protester = data.driverMap[protest.protestingDriverId]; - const accused = data.driverMap[protest.accusedDriverId]; - const daysSinceFiled = Math.floor((Date.now() - new Date(protest.filedAt).getTime()) / (1000 * 60 * 60 * 24)); - const isUrgent = daysSinceFiled > 2 && (protest.status === 'pending' || protest.status === 'under_review'); - - return ( -
-
-
-
- - - {protester?.name || 'Unknown'} vs {accused?.name || 'Unknown'} - - {getStatusBadge(protest.status)} - {isUrgent && ( - - - {daysSinceFiled}d old - - )} -
-
- Lap {protest.incident.lap} - - Filed {new Date(protest.filedAt).toLocaleDateString()} - {protest.proofVideoUrl && ( - <> - - - - - )} -
-

- {protest.incident.description} -

- {protest.decisionNotes && ( -
-

- Steward: {protest.decisionNotes} -

-
- )} -
- {(protest.status === 'pending' || protest.status === 'under_review') && ( - - - - )} -
-
- ); - })} - - {activeTab === 'history' && penalties.map((penalty) => { - const driver = data.driverMap[penalty.driverId]; - return ( -
-
-
- -
-
-
- {driver?.name || 'Unknown'} - - {penalty.type.replace('_', ' ')} - -
-

{penalty.reason}

-
-
- - {penalty.type === 'time_penalty' && `+${penalty.value}s`} - {penalty.type === 'grid_penalty' && `+${penalty.value} grid`} - {penalty.type === 'points_deduction' && `-${penalty.value} pts`} - {penalty.type === 'disqualification' && 'DSQ'} - {penalty.type === 'warning' && 'Warning'} - {penalty.type === 'license_points' && `${penalty.value} LP`} - -
-
-
- ); - })} - - )} -
- )} -
- ); - })} -
+ )} diff --git a/apps/website/components/leagues/CreateLeagueWizard.tsx b/apps/website/app/leagues/create/CreateLeagueWizard.tsx similarity index 100% rename from apps/website/components/leagues/CreateLeagueWizard.tsx rename to apps/website/app/leagues/create/CreateLeagueWizard.tsx diff --git a/apps/website/components/onboarding/OnboardingWizardClient.tsx b/apps/website/app/onboarding/OnboardingWizardClient.tsx similarity index 100% rename from apps/website/components/onboarding/OnboardingWizardClient.tsx rename to apps/website/app/onboarding/OnboardingWizardClient.tsx diff --git a/apps/website/app/profile/liveries/page.tsx b/apps/website/app/profile/liveries/page.tsx index 3e42e710e..9d48541ce 100644 --- a/apps/website/app/profile/liveries/page.tsx +++ b/apps/website/app/profile/liveries/page.tsx @@ -1,23 +1,55 @@ import Link from 'next/link'; -import Button from '@/ui/Button'; -import Card from '@/ui/Card'; -import Container from '@/ui/Container'; -import Heading from '@/ui/Heading'; +import { Button } from '@/ui/Button'; +import { Card } from '@/ui/Card'; +import { Container } from '@/ui/Container'; +import { Heading } from '@/ui/Heading'; +import { Grid } from '@/ui/Grid'; import { routes } from '@/lib/routing/RouteConfig'; +import { LiveryCard } from '@/components/profile/LiveryCard'; export default async function ProfileLiveriesPage() { + const mockLiveries = [ + { + id: '1', + carId: 'gt3-r', + carName: 'Porsche 911 GT3 R (992)', + thumbnailUrl: '', + uploadedAt: new Date(), + isValidated: true, + }, + { + id: '2', + carId: 'f3', + carName: 'Dallara F3', + thumbnailUrl: '', + uploadedAt: new Date(), + isValidated: false, + } + ]; + return ( - - Liveries - -

Livery management is currently unavailable.

- - - + +
+
+ My Liveries +

Manage your custom car liveries

+
- +
+ + + {mockLiveries.map((livery) => ( + + ))} + + +
+ + + +
); } diff --git a/apps/website/components/TeamRankingsFilter.tsx b/apps/website/components/TeamRankingsFilter.tsx index 72bc6f5e5..c68895c1a 100644 --- a/apps/website/components/TeamRankingsFilter.tsx +++ b/apps/website/components/TeamRankingsFilter.tsx @@ -1,7 +1,12 @@ import React from 'react'; -import { Search, Star, Trophy, Percent, Hash } from 'lucide-react'; -import Button from '@/ui/Button'; -import Input from '@/ui/Input'; +import { Search, Star, Trophy, Percent, Hash, LucideIcon } from 'lucide-react'; +import { Button } from '@/ui/Button'; +import { Input } from '@/ui/Input'; +import { Stack } from '@/ui/Stack'; +import { Box } from '@/ui/Box'; +import { Text } from '@/ui/Text'; +import { Icon } from '@/ui/Icon'; +import { Badge } from '@/ui/Badge'; type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner'; type SortBy = 'rating' | 'wins' | 'winRate' | 'races'; @@ -9,17 +14,15 @@ type SortBy = 'rating' | 'wins' | 'winRate' | 'races'; const SKILL_LEVELS: { id: SkillLevel; label: string; - color: string; - bgColor: string; - borderColor: string; + variant: 'warning' | 'primary' | 'info' | 'success'; }[] = [ - { id: 'pro', label: 'Pro', color: 'text-yellow-400', bgColor: 'bg-yellow-400/10', borderColor: 'border-yellow-400/30' }, - { id: 'advanced', label: 'Advanced', color: 'text-purple-400', bgColor: 'bg-purple-400/10', borderColor: 'border-purple-400/30' }, - { id: 'intermediate', label: 'Intermediate', color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', borderColor: 'border-primary-blue/30' }, - { id: 'beginner', label: 'Beginner', color: 'text-green-400', bgColor: 'bg-green-400/10', borderColor: 'border-green-400/30' }, + { id: 'pro', label: 'Pro', variant: 'warning' }, + { id: 'advanced', label: 'Advanced', variant: 'primary' }, + { id: 'intermediate', label: 'Intermediate', variant: 'info' }, + { id: 'beginner', label: 'Beginner', variant: 'success' }, ]; -const SORT_OPTIONS: { id: SortBy; label: string; icon: React.ElementType }[] = [ +const SORT_OPTIONS: { id: SortBy; label: string; icon: LucideIcon }[] = [ { id: 'rating', label: 'Rating', icon: Star }, { id: 'wins', label: 'Total Wins', icon: Trophy }, { id: 'winRate', label: 'Win Rate', icon: Percent }, @@ -35,7 +38,7 @@ interface TeamRankingsFilterProps { onSortChange: (sort: SortBy) => void; } -export default function TeamRankingsFilter({ +export function TeamRankingsFilter({ searchQuery, onSearchChange, filterLevel, @@ -44,76 +47,70 @@ export default function TeamRankingsFilter({ onSortChange, }: TeamRankingsFilterProps) { return ( -
+ {/* Search and Level Filter Row */} -
-
- + + onSearchChange(e.target.value)} - className="pl-11" + icon={} /> -
+ {/* Level Filter */} -
- + {SKILL_LEVELS.map((level) => { + const isActive = filterLevel === level.id; return ( - + {isActive ? ( + {level.label} + ) : ( + level.label + )} + ); })} -
-
+
+ {/* Sort Options */} -
- Sort by: -
- {SORT_OPTIONS.map((option) => { - const OptionIcon = option.icon; - return ( - - ); - })} -
-
-
+ + Sort by: + + + {SORT_OPTIONS.map((option) => { + const isActive = sortBy === option.id; + return ( + + ); + })} + + + + ); -} \ No newline at end of file +} diff --git a/apps/website/components/achievements/AchievementCard.tsx b/apps/website/components/achievements/AchievementCard.tsx deleted file mode 100644 index 9da166650..000000000 --- a/apps/website/components/achievements/AchievementCard.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import { Trophy, Medal, Star, Crown, Target, Zap } from 'lucide-react'; -import type { DriverProfileAchievementViewModel } from '@/lib/view-models/DriverProfileViewModel'; - -interface AchievementCardProps { - achievement: DriverProfileAchievementViewModel; -} - -function getRarityColor(rarity: DriverProfileAchievementViewModel['rarity']) { - switch (rarity) { - case 'common': - return 'text-gray-400 bg-gray-400/10 border-gray-400/30'; - case 'rare': - return 'text-primary-blue bg-primary-blue/10 border-primary-blue/30'; - case 'epic': - return 'text-purple-400 bg-purple-400/10 border-purple-400/30'; - case 'legendary': - return 'text-yellow-400 bg-yellow-400/10 border-yellow-400/30'; - } -} - -function getAchievementIcon(icon: DriverProfileAchievementViewModel['icon']) { - switch (icon) { - case 'trophy': - return Trophy; - case 'medal': - return Medal; - case 'star': - return Star; - case 'crown': - return Crown; - case 'target': - return Target; - case 'zap': - return Zap; - } -} - -export default function AchievementCard({ achievement }: AchievementCardProps) { - const Icon = getAchievementIcon(achievement.icon); - const rarityClasses = getRarityColor(achievement.rarity); - - return ( -
-
-
- -
-
-

{achievement.title}

-

{achievement.description}

-

- {new Date(achievement.earnedAt).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - })} -

-
-
-
- ); -} \ No newline at end of file diff --git a/apps/website/components/admin/UserFilters.tsx b/apps/website/components/admin/UserFilters.tsx index 21b2ca6b5..ae7b83ce8 100644 --- a/apps/website/components/admin/UserFilters.tsx +++ b/apps/website/components/admin/UserFilters.tsx @@ -4,7 +4,6 @@ import React from 'react'; import { Filter, Search } from 'lucide-react'; import { Card } from '@/ui/Card'; import { Stack } from '@/ui/Stack'; -import { Box } from '@/ui/Box'; import { Text } from '@/ui/Text'; import { Button } from '@/ui/Button'; import { Grid } from '@/ui/Grid'; @@ -51,18 +50,13 @@ export function UserFilters({ - - - - - onSearch(e.target.value)} - style={{ paddingLeft: '2.25rem' }} - /> - + onSearch(e.target.value)} + icon={} + /> { - setEmail(e.target.value); - if (feedback.type !== 'loading') { - setFeedback({ type: 'idle' }); - } - }} - placeholder="your@email.com" - disabled={feedback.type === 'loading'} - className={`w-full px-6 py-4 rounded-lg bg-iron-gray text-white placeholder-gray-500 border transition-all duration-150 ${ - feedback.type === 'error' && !feedback.retryAfter - ? 'border-red-500 focus:ring-2 focus:ring-red-500' - : feedback.type === 'error' && feedback.retryAfter - ? 'border-warning-amber focus:ring-2 focus:ring-warning-amber/50' - : 'border-charcoal-outline focus:border-neon-aqua focus:ring-2 focus:ring-neon-aqua/50' - } hover:scale-[1.01] disabled:opacity-50 disabled:cursor-not-allowed`} - aria-label="Email address" - /> - - {(feedback.type === 'error' || feedback.type === 'info') && ( - - {feedback.message} - {feedback.type === 'error' && feedback.retryAfter && ( - - Retry in {feedback.retryAfter}s - - )} - - )} - -
- - {feedback.type === 'loading' ? ( - - - - - - Joining... - - ) : ( - 'Count me in' - )} - -
- - - -
- - - - I'll send updates as I build -
-
- - - - You can tell me what matters most -
-
- - - - Zero spam, zero BS -
-
- - )} - -
- - ); -} \ No newline at end of file diff --git a/apps/website/components/layout/Breadcrumbs.tsx b/apps/website/components/layout/Breadcrumbs.tsx index cb73f1499..0fc8f1c66 100644 --- a/apps/website/components/layout/Breadcrumbs.tsx +++ b/apps/website/components/layout/Breadcrumbs.tsx @@ -1,6 +1,8 @@ -'use client'; - -import Link from 'next/link'; +import React from 'react'; +import { Link } from '@/ui/Link'; +import { Box } from '@/ui/Box'; +import { Stack } from '@/ui/Stack'; +import { Text } from '@/ui/Text'; export type BreadcrumbItem = { label: string; @@ -12,7 +14,7 @@ interface BreadcrumbsProps { className?: string; } -export default function Breadcrumbs({ items, className }: BreadcrumbsProps) { +export function Breadcrumbs({ items }: BreadcrumbsProps) { if (!items || items.length === 0) { return null; } @@ -20,34 +22,31 @@ export default function Breadcrumbs({ items, className }: BreadcrumbsProps) { const lastIndex = items.length - 1; return ( - + + ); } \ No newline at end of file diff --git a/apps/website/components/leagues/BonusPointsCard.tsx b/apps/website/components/leagues/BonusPointsCard.tsx deleted file mode 100644 index 7526bdd64..000000000 --- a/apps/website/components/leagues/BonusPointsCard.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import Card from '@/ui/Card'; - -interface BonusPointsCardProps { - bonusSummary: string[]; -} - -export function BonusPointsCard({ bonusSummary }: BonusPointsCardProps) { - if (!bonusSummary || bonusSummary.length === 0) { - return null; - } - - return ( - -
-

Bonus Points

-

Additional points for special achievements

-
- -
- {bonusSummary.map((bonus, idx) => ( -
-
- + -
-

{bonus}

-
- ))} -
-
- ); -} \ No newline at end of file diff --git a/apps/website/components/leagues/ChampionshipCard.tsx b/apps/website/components/leagues/ChampionshipCard.tsx deleted file mode 100644 index dceb4facc..000000000 --- a/apps/website/components/leagues/ChampionshipCard.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import Card from '@/ui/Card'; -import type { LeagueScoringChampionshipViewModel } from '@/lib/view-models/LeagueScoringChampionshipViewModel'; - -type PointsPreviewRow = { - sessionType: string; - position: number; - points: number; -}; - -interface ChampionshipCardProps { - championship: LeagueScoringChampionshipViewModel; -} - -export function ChampionshipCard({ championship }: ChampionshipCardProps) { - const pointsPreview = (championship.pointsPreview as unknown as PointsPreviewRow[]) ?? []; - const dropPolicyDescription = (championship as unknown as { dropPolicyDescription?: string }).dropPolicyDescription ?? ''; - const getTypeLabel = (type: string): string => { - switch (type) { - case 'driver': - return 'Driver Championship'; - case 'team': - return 'Team Championship'; - case 'nations': - return 'Nations Championship'; - case 'trophy': - return 'Trophy Championship'; - default: - return 'Championship'; - } - }; - - const getTypeBadgeStyle = (type: string): string => { - switch (type) { - case 'driver': - return 'bg-primary-blue/10 text-primary-blue border-primary-blue/20'; - case 'team': - return 'bg-purple-500/10 text-purple-400 border-purple-500/20'; - case 'nations': - return 'bg-performance-green/10 text-performance-green border-performance-green/20'; - case 'trophy': - return 'bg-warning-amber/10 text-warning-amber border-warning-amber/20'; - default: - return 'bg-gray-500/10 text-gray-400 border-gray-500/20'; - } - }; - - return ( - -
-
-

{championship.name}

- - {getTypeLabel(championship.type)} - -
-
- -
- {/* Session Types */} - {championship.sessionTypes.length > 0 && ( -
-

Scored Sessions

-
- {championship.sessionTypes.map((session, idx) => ( - - {session} - - ))} -
-
- )} - - {/* Points Preview */} - {pointsPreview.length > 0 && ( -
-

Points Distribution

-
-
- {pointsPreview.slice(0, 6).map((preview, idx) => ( -
-
P{preview.position}
-
{preview.points}
-
- ))} -
-
-
- )} - - {/* Drop Policy */} -
-
- Drop Policy -
-

{dropPolicyDescription}

-
-
-
- ); -} diff --git a/apps/website/components/leagues/CreateLeagueForm.tsx b/apps/website/components/leagues/CreateLeagueForm.tsx deleted file mode 100644 index 2f5f52b65..000000000 --- a/apps/website/components/leagues/CreateLeagueForm.tsx +++ /dev/null @@ -1,192 +0,0 @@ -'use client'; - -import { useState, FormEvent } from 'react'; -import { useRouter } from 'next/navigation'; -import Input from '../ui/Input'; -import Button from '../ui/Button'; -import { useCreateLeague } from "@/lib/hooks/league/useCreateLeague"; -import { useAuth } from '@/lib/auth/AuthContext'; -import { useInject } from '@/lib/di/hooks/useInject'; -import { DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens'; - -interface FormErrors { - name?: string; - description?: string; - pointsSystem?: string; - sessionDuration?: string; - submit?: string; -} - -export default function CreateLeagueForm() { - const router = useRouter(); - const [errors, setErrors] = useState({}); - - const [formData, setFormData] = useState({ - name: '', - description: '', - pointsSystem: 'f1-2024' as 'f1-2024' | 'indycar', - sessionDuration: 60 - }); - - const validateForm = (): boolean => { - const newErrors: FormErrors = {}; - - if (!formData.name.trim()) { - newErrors.name = 'Name is required'; - } else if (formData.name.length > 100) { - newErrors.name = 'Name must be 100 characters or less'; - } - - if (!formData.description.trim()) { - newErrors.description = 'Description is required'; - } else if (formData.description.length > 500) { - newErrors.description = 'Description must be 500 characters or less'; - } - - if (formData.sessionDuration < 1 || formData.sessionDuration > 240) { - newErrors.sessionDuration = 'Session duration must be between 1 and 240 minutes'; - } - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }; - - const { session } = useAuth(); - const driverService = useInject(DRIVER_SERVICE_TOKEN); - const createLeagueMutation = useCreateLeague(); - - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - - if (createLeagueMutation.isPending) return; - - if (!validateForm()) return; - - if (!session?.user.userId) { - setErrors({ submit: 'You must be logged in to create a league' }); - return; - } - - try { - // Get current driver - const currentDriver = await driverService.getDriverProfile(session.user.userId); - - if (!currentDriver) { - setErrors({ submit: 'No driver profile found. Please create a profile first.' }); - return; - } - - // Create league using the league service - const input = { - name: formData.name.trim(), - description: formData.description.trim(), - visibility: 'public' as const, - ownerId: session.user.userId, - }; - - const result = await createLeagueMutation.mutateAsync(input); - router.push(`/leagues/${result.leagueId}`); - router.refresh(); - } catch (error) { - setErrors({ - submit: error instanceof Error ? error.message : 'Failed to create league' - }); - } - }; - - return ( - <> -
-
- - setFormData({ ...formData, name: e.target.value })} - error={!!errors.name} - errorMessage={errors.name} - placeholder="European GT Championship" - maxLength={100} - disabled={createLeagueMutation.isPending} - /> -

- {formData.name.length}/100 -

-
- -
- -