From 3556db494fa768c2d4e57aedf7f133e10e9b7d9e Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Tue, 20 Jan 2026 01:30:17 +0100 Subject: [PATCH] website refactor --- .../leagues}/LeaguesPageClient.tsx | 25 ++-- apps/website/app/leagues/page.tsx | 2 +- .../website/components/leagues/LeagueCard.tsx | 114 +++++++------- .../components/leagues/LeagueCardWrapper.tsx | 10 +- apps/website/templates/LeaguesTemplate.tsx | 139 ++++++++++-------- apps/website/ui/LeagueCard.tsx | 82 ++++++++--- 6 files changed, 210 insertions(+), 162 deletions(-) rename apps/website/{client-wrapper => app/leagues}/LeaguesPageClient.tsx (86%) diff --git a/apps/website/client-wrapper/LeaguesPageClient.tsx b/apps/website/app/leagues/LeaguesPageClient.tsx similarity index 86% rename from apps/website/client-wrapper/LeaguesPageClient.tsx rename to apps/website/app/leagues/LeaguesPageClient.tsx index 82f5379ae..d61799af8 100644 --- a/apps/website/client-wrapper/LeaguesPageClient.tsx +++ b/apps/website/app/leagues/LeaguesPageClient.tsx @@ -24,37 +24,35 @@ const CATEGORIES: Category[] = [ id: 'all', label: 'All', icon: Globe, - description: 'Browse all available leagues', + description: 'All available competition infrastructure.', filter: () => true, }, { id: 'popular', label: 'Popular', icon: Flame, - description: 'Most active leagues right now', + description: 'High utilization infrastructure.', filter: (league) => { const fillRate = (league.usedDriverSlots ?? 0) / (league.maxDrivers ?? 1); return fillRate > 0.7; }, - color: 'text-orange-400', }, { id: 'new', label: 'New', icon: Sparkles, - description: 'Fresh leagues looking for members', + description: 'Recently deployed infrastructure.', filter: (league) => { const oneWeekAgo = new Date(); oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); return new Date(league.createdAt) > oneWeekAgo; }, - color: 'text-green-500', }, { id: 'openSlots', - label: 'Open Slots', + label: 'Open', icon: Target, - description: 'Leagues with available spots', + description: 'Infrastructure with available capacity.', filter: (league) => { if (league.maxTeams && league.maxTeams > 0) { const usedTeams = league.usedTeamSlots ?? 0; @@ -64,41 +62,40 @@ const CATEGORIES: Category[] = [ const max = league.maxDrivers ?? 0; return max > 0 && used < max; }, - color: 'text-cyan-400', }, { id: 'driver', label: 'Driver', icon: Trophy, - description: 'Compete as an individual', + description: 'Individual competition format.', filter: (league) => league.scoring?.primaryChampionshipType === 'driver', }, { id: 'team', label: 'Team', icon: Users, - description: 'Race together as a team', + description: 'Team-based competition format.', filter: (league) => league.scoring?.primaryChampionshipType === 'team', }, { id: 'nations', label: 'Nations', icon: Flag, - description: 'Represent your country', + description: 'National representation format.', filter: (league) => league.scoring?.primaryChampionshipType === 'nations', }, { id: 'trophy', label: 'Trophy', icon: Award, - description: 'Special championship events', + description: 'Special event infrastructure.', filter: (league) => league.scoring?.primaryChampionshipType === 'trophy', }, { id: 'endurance', label: 'Endurance', icon: Timer, - description: 'Long-distance racing', + description: 'Long-duration competition.', filter: (league) => league.scoring?.scoringPresetId?.includes('endurance') ?? league.timingSummary?.includes('h Race') ?? @@ -108,7 +105,7 @@ const CATEGORIES: Category[] = [ id: 'sprint', label: 'Sprint', icon: Clock, - description: 'Quick, intense races', + description: 'Short-duration competition.', filter: (league) => (league.scoring?.scoringPresetId?.includes('sprint') ?? false) && !(league.scoring?.scoringPresetId?.includes('endurance') ?? false), diff --git a/apps/website/app/leagues/page.tsx b/apps/website/app/leagues/page.tsx index b53b5bea7..b06a7e449 100644 --- a/apps/website/app/leagues/page.tsx +++ b/apps/website/app/leagues/page.tsx @@ -1,5 +1,5 @@ import { notFound } from 'next/navigation'; -import { LeaguesPageClient } from '@/client-wrapper/LeaguesPageClient'; +import { LeaguesPageClient } from './LeaguesPageClient'; import { LeaguesPageQuery } from '@/lib/page-queries/LeaguesPageQuery'; export default async function Page() { diff --git a/apps/website/components/leagues/LeagueCard.tsx b/apps/website/components/leagues/LeagueCard.tsx index f853600fc..067956764 100644 --- a/apps/website/components/leagues/LeagueCard.tsx +++ b/apps/website/components/leagues/LeagueCard.tsx @@ -8,8 +8,9 @@ import { Text } from '@/ui/Text'; import { Box } from '@/ui/Box'; import { Group } from '@/ui/Group'; import { Surface } from '@/ui/Surface'; +import { Stack } from '@/ui/Stack'; import { LeagueCard as UILeagueCard, LeagueCardStats, LeagueCardFooter } from '@/ui/LeagueCard'; -import { Calendar as LucideCalendar } from 'lucide-react'; +import { Calendar, Users, Activity } from 'lucide-react'; import React, { ReactNode } from 'react'; interface LeagueCardProps { @@ -54,80 +55,81 @@ export function LeagueCard({ coverUrl={coverUrl} logo={ {logoUrl ? ( {`${name} ) : ( - + )} } badges={ - + {badges} {championshipBadge} - + } > - - - - {name} - - - - - {description || 'No description available'} - - - = 90 ? 'warning' : fillPercentage >= 70 ? 'primary' : 'success'} - /> - - {hasOpenSlots && ( - - - - {openSlotsCount} OPEN - - - )} - - - {timingSummary && ( - - - - {timingSummary.split('•')[1]?.trim() || timingSummary} - + + + + + {name} - )} - + + {description || 'No infrastructure description provided.'} + + + + + = 90 ? 'warning' : fillPercentage >= 70 ? 'primary' : 'success'} + /> + + {hasOpenSlots && ( + + + + {openSlotsCount} slots available + + + )} + + + + + {timingSummary && ( + + + + {timingSummary.split('•')[1]?.trim() || timingSummary} + + + )} + + + + {usedSlots} active participants + + + + + ); } diff --git a/apps/website/components/leagues/LeagueCardWrapper.tsx b/apps/website/components/leagues/LeagueCardWrapper.tsx index f51bd8022..a2d435b0a 100644 --- a/apps/website/components/leagues/LeagueCardWrapper.tsx +++ b/apps/website/components/leagues/LeagueCardWrapper.tsx @@ -5,6 +5,8 @@ import { Flag, Award, Sparkles, + Gamepad2, + Layers, } from 'lucide-react'; import type { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel'; import { getMediaUrl } from '@/lib/utilities/media'; @@ -152,24 +154,24 @@ export function LeagueCard({ league, onClick }: LeagueCardProps) { badges={ <> {isNew && ( - + NEW )} {league.scoring?.gameName && ( - + {league.scoring.gameName} )} {league.category && ( - + {categoryLabel} )} } championshipBadge={ - + {championshipLabel} } diff --git a/apps/website/templates/LeaguesTemplate.tsx b/apps/website/templates/LeaguesTemplate.tsx index 5de2d90d4..83118d228 100644 --- a/apps/website/templates/LeaguesTemplate.tsx +++ b/apps/website/templates/LeaguesTemplate.tsx @@ -3,7 +3,7 @@ import { LeagueCard } from '@/components/leagues/LeagueCardWrapper'; import type { LeaguesViewData } from '@/lib/view-data/LeaguesViewData'; import { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel'; -import { Heading } from '@/ui/Heading'; +import { PageHeader } from '@/ui/PageHeader'; import { Input } from '@/ui/Input'; import { Button } from '@/ui/Button'; import { Group } from '@/ui/Group'; @@ -12,11 +12,16 @@ import { Container } from '@/ui/Container'; import { Text } from '@/ui/Text'; import { Icon } from '@/ui/Icon'; import { Section } from '@/ui/Section'; -import { StatusDot } from '@/ui/StatusDot'; +import { ControlBar } from '@/ui/ControlBar'; +import { SegmentedControl } from '@/ui/SegmentedControl'; +import { MetricCard } from '@/ui/MetricCard'; +import { Stack } from '@/ui/Stack'; +import { Box } from '@/ui/Box'; import { Plus, Search, Trophy, + Filter, type LucideIcon, } from 'lucide-react'; import React from 'react'; @@ -68,29 +73,14 @@ export function LeaguesTemplate({ onClearFilters, }: LeaguesTemplateProps) { return ( - - - {/* Hero */} - - - - - Competition Hub - - - Find Your Grid - - - From casual sprints to epic endurance battles — discover the perfect league for your racing style. - - - - - - {viewData.leagues.length} - Active Leagues - - + + + {/* Header Section */} + Create League - - + } + /> - {/* Search & Filters */} - - ) => onSearchChange(e.target.value)} - icon={} + {/* Stats Overview */} + + + acc + (l.usedDriverSlots ?? 0), 0)} + icon={Trophy} + intent="primary" + /> + acc + Math.max(0, (l.maxDrivers ?? 0) - (l.usedDriverSlots ?? 0)), 0)} + icon={Trophy} + intent="success" + /> + - - {categories.map((category) => { - const isActive = activeCategory === category.id; - const CategoryIcon = category.icon; - return ( - - ); - })} - - + {/* Control Bar */} + + + ({ + id: c.id, + label: c.label, + icon: + }))} + activeId={activeCategory} + onChange={(id) => onCategoryChange(id as CategoryId)} + /> + + } + > + + ) => onSearchChange(e.target.value)} + icon={} + size="sm" + /> + + - {/* Grid */} - + {/* Results */} + {filteredLeagues.length > 0 ? ( {filteredLeagues.map((league) => ( @@ -145,22 +157,23 @@ export function LeaguesTemplate({ ) : (
- + - No Leagues Found - Try adjusting your search or filters + + No results found + Adjust filters to find matching infrastructure. + - +
)} -
- + +
); } diff --git a/apps/website/ui/LeagueCard.tsx b/apps/website/ui/LeagueCard.tsx index 071be6783..0f8f10b7d 100644 --- a/apps/website/ui/LeagueCard.tsx +++ b/apps/website/ui/LeagueCard.tsx @@ -5,6 +5,8 @@ import { Card } from './Card'; import { Icon } from './Icon'; import { Image } from './Image'; import { Text } from './Text'; +import { Stack } from './Stack'; +import { Group } from './Group'; export interface LeagueCardProps { children: ReactNode; @@ -17,23 +19,37 @@ export interface LeagueCardProps { export const LeagueCard = ({ children, onClick, coverUrl, logo, badges }: LeagueCardProps) => { return ( - - Cover - - - {badges} + + Cover + + + + {badges} + {logo && ( - + {logo} )} - + {children} @@ -44,7 +60,7 @@ export interface LeagueCardStatsProps { label: string; value: string; percentage: number; - intent?: 'primary' | 'success' | 'warning'; + intent?: 'primary' | 'success' | 'warning' | 'telemetry'; } export const LeagueCardStats = ({ label, value, percentage, intent = 'primary' }: LeagueCardStatsProps) => { @@ -52,17 +68,27 @@ export const LeagueCardStats = ({ label, value, percentage, intent = 'primary' } primary: 'var(--ui-color-intent-primary)', success: 'var(--ui-color-intent-success)', warning: 'var(--ui-color-intent-warning)', + telemetry: 'var(--ui-color-intent-telemetry)', }; return ( - - - {label} - {value} - - - - + + + + {label} + {value} + + + + + ); }; @@ -72,11 +98,19 @@ export interface LeagueCardFooterProps { } export const LeagueCardFooter = ({ children }: LeagueCardFooterProps) => ( - - {children} - - VIEW - - + + + + {children} + + + ACCESS + + + );