diff --git a/apps/website/client-wrapper/TeamsPageClient.tsx b/apps/website/client-wrapper/TeamsPageClient.tsx index dddab8570..dd35ce9ea 100644 --- a/apps/website/client-wrapper/TeamsPageClient.tsx +++ b/apps/website/client-wrapper/TeamsPageClient.tsx @@ -1,6 +1,6 @@ 'use client'; -import React from 'react'; +import React, { useState } from 'react'; import { useRouter } from 'next/navigation'; import { TeamsTemplate } from '@/templates/TeamsTemplate'; import type { TeamsViewData } from '@/lib/view-data/TeamsViewData'; @@ -12,6 +12,7 @@ interface TeamsPageClientProps { export function TeamsPageClient({ viewData }: TeamsPageClientProps) { const router = useRouter(); + const [searchQuery, setSearchQuery] = useState(''); const handleTeamClick = (teamId: string) => { router.push(`/teams/${teamId}`); @@ -28,6 +29,8 @@ export function TeamsPageClient({ viewData }: TeamsPageClientProps) { return ( { setEditMode(false); @@ -96,17 +92,22 @@ export function TeamAdmin({ team, onUpdate }: TeamAdminProps) { }; return ( - + setEditMode(true)}> - Edit Details - - )} + variant="default" + padding="md" > + + Team Settings + {!editMode && ( + + )} + + {editMode ? ( - + setEditedTeam({ ...editedTeam, description: e.target.value })} /> - + @@ -145,27 +146,30 @@ export function TeamAdmin({ team, onUpdate }: TeamAdminProps) { > Cancel - + ) : ( - - - Team Name - {team.name} + + + Team Name + {team.name} - - Team Tag - {team.tag} + + Team Tag + {team.tag} - - Description - {team.description} + + Description + {team.description} )} - + + + Join Requests + {loading ? ( ) : joinRequests.length > 0 ? ( @@ -202,3 +206,5 @@ export function TeamAdmin({ team, onUpdate }: TeamAdminProps) { ); } + +import { Heading } from '@/ui/Heading'; diff --git a/apps/website/components/teams/TeamCard.tsx b/apps/website/components/teams/TeamCard.tsx index 092e5cc2c..fb93b82b7 100644 --- a/apps/website/components/teams/TeamCard.tsx +++ b/apps/website/components/teams/TeamCard.tsx @@ -1,56 +1,71 @@ 'use client'; - -import { TeamLogo } from '@/components/teams/TeamLogo'; -import { TeamCard as UITeamCard } from '@/ui/TeamCard'; -import React, { ReactNode } from 'react'; - + +import React from 'react'; +import { TeamCard as UiTeamCard } from '@/ui/TeamCard'; +import { TeamSummaryData } from '@/lib/view-data/TeamsViewData'; +import { Image } from '@/ui/Image'; + interface TeamCardProps { - name: string; - description?: string; + team?: TeamSummaryData; + // Compatibility props + name?: string; + leagueName?: string; logo?: string; - memberCount: number; - isRecruiting?: boolean; - performanceBadge?: ReactNode; - specializationContent?: ReactNode; - categoryBadge?: ReactNode; + memberCount?: number; + ratingLabel?: string; + winsLabel?: string; + racesLabel?: string; region?: string; - languagesContent?: ReactNode; - statsContent?: ReactNode; - onClick?: () => void; + isRecruiting?: boolean; + performanceLevel?: string; + description?: string; + onClick?: (id: string) => void; } - -export function TeamCard({ + +export function TeamCard({ + team, name, - description, + leagueName, logo, memberCount, - isRecruiting, - performanceBadge, - specializationContent, - categoryBadge, + ratingLabel, + winsLabel, + racesLabel, region, - languagesContent, - onClick, + isRecruiting, + performanceLevel, + description, + onClick }: TeamCardProps) { + const data = team || { + teamId: '', + teamName: name || '', + leagueName: leagueName || '', + memberCount: memberCount || 0, + logoUrl: logo, + ratingLabel: ratingLabel || '-', + winsLabel: winsLabel || '-', + racesLabel: racesLabel || '-', + region: region, + isRecruiting: isRecruiting || false, + performanceLevel: performanceLevel, + description: description, + }; + return ( - - } - badges={ - <> - {performanceBadge} - {specializationContent} - {categoryBadge} - {languagesContent} - - } + : undefined} + memberCount={data.memberCount} + rating={data.ratingLabel} + wins={data.winsLabel} + races={data.racesLabel} + region={data.region} + isRecruiting={data.isRecruiting} + performanceLevel={data.performanceLevel} + description={data.description} + onClick={() => onClick?.(data.teamId)} /> ); } diff --git a/apps/website/components/teams/TeamCardWrapper.tsx b/apps/website/components/teams/TeamCardWrapper.tsx index 64e794eb0..f7ab9594b 100644 --- a/apps/website/components/teams/TeamCardWrapper.tsx +++ b/apps/website/components/teams/TeamCardWrapper.tsx @@ -4,11 +4,9 @@ import { Badge } from '@/ui/Badge'; import { Icon } from '@/ui/Icon'; import { Group } from '@/ui/Group'; import { Text } from '@/ui/Text'; -import { BadgeGroup } from '@/ui/BadgeGroup'; import { Clock, Crown, - Languages, Shield, Star, TrendingUp, @@ -34,35 +32,8 @@ interface TeamCardProps { onClick?: () => void; } -function getPerformanceBadge(level?: string) { - switch (level) { - case 'pro': - return { icon: Crown, label: 'Pro', variant: 'warning' as const }; - case 'advanced': - return { icon: Star, label: 'Advanced', variant: 'primary' as const }; - case 'intermediate': - return { icon: TrendingUp, label: 'Intermediate', variant: 'default' as const }; - case 'beginner': - return { icon: Shield, label: 'Beginner', variant: 'success' as const }; - default: - return null; - } -} - -function getSpecializationBadge(specialization?: string) { - switch (specialization) { - case 'endurance': - return { icon: Clock, label: 'Endurance', intent: 'warning' as const }; - case 'sprint': - return { icon: Zap, label: 'Sprint', intent: 'telemetry' as const }; - default: - return null; - } -} - export function TeamCard({ name, - description, logo, memberCount, ratingLabel, @@ -70,53 +41,21 @@ export function TeamCard({ racesLabel, performanceLevel, isRecruiting, - specialization, region, - languages, - category, onClick, }: TeamCardProps) { - const performanceBadge = getPerformanceBadge(performanceLevel); - const specializationBadge = getSpecializationBadge(specialization); - return ( - {performanceBadge.label} - - )} - specializationContent={specializationBadge && ( - - - {specializationBadge.label} - - )} - categoryBadge={category && ( - - {category} - - )} - languagesContent={languages && languages.length > 0 && ( - - {languages.slice(0, 2).join(', ')} - {languages.length > 2 && ` +${languages.length - 2}`} - - )} - statsContent={ - - - - - - } + performanceLevel={performanceLevel} /> ); } diff --git a/apps/website/components/teams/TeamDetailsHeader.tsx b/apps/website/components/teams/TeamDetailsHeader.tsx index 042dbdb94..2414fe5a6 100644 --- a/apps/website/components/teams/TeamDetailsHeader.tsx +++ b/apps/website/components/teams/TeamDetailsHeader.tsx @@ -1,15 +1,15 @@ 'use client'; - -import { Button } from '@/ui/Button'; -import { Image } from '@/ui/Image'; -import { TeamHero } from '@/ui/TeamHero'; -import { Text } from '@/ui/Text'; -import { Badge } from '@/ui/Badge'; -import { StatGrid } from '@/ui/StatGrid'; -import { Group } from '@/ui/Group'; -import { Surface } from '@/ui/Surface'; + +import React from 'react'; +import { Users, Calendar, Shield, Settings, Globe } from 'lucide-react'; import { Box } from '@/ui/Box'; - +import { Heading } from '@/ui/Heading'; +import { Text } from '@/ui/Text'; +import { Stack } from '@/ui/Stack'; +import { Button } from '@/ui/Button'; +import { Surface } from '@/ui/Surface'; +import { Icon } from '@/ui/Icon'; + interface TeamDetailsHeaderProps { teamId: string; name: string; @@ -17,81 +17,126 @@ interface TeamDetailsHeaderProps { description?: string; logoUrl?: string; memberCount: number; - memberCountLabel?: string; - foundedDate?: string; foundedDateLabel?: string; isAdmin?: boolean; onAdminClick?: () => void; } - + export function TeamDetailsHeader({ name, tag, description, logoUrl, memberCount, - memberCountLabel, - foundedDate, foundedDateLabel, isAdmin, onAdminClick, }: TeamDetailsHeaderProps) { return ( - - {name} - {tag && [{tag}]} - - } - description={description || 'No mission statement provided.'} - sideContent={ - - {logoUrl ? ( - {name} - ) : ( - {name.substring(0, 2).toUpperCase()} - )} - - } - stats={ - - } - actions={ - - {isAdmin && ( - - )} - - - } - /> + + + + + {/* Logo Container */} + + {logoUrl ? ( + + ) : ( + + + {name.substring(0, 3).toUpperCase()} + + )} + + + {/* Info Section */} + + + + + {name} + {tag && ( + + {tag} + + )} + + + + {description || 'No mission statement provided.'} + + + + + {/* Metadata Grid */} + + + + + + + + Personnel + + {memberCount} UNITS + + + + + + + + + + Established + + {foundedDateLabel || 'UNKNOWN'} + + + + + + + + + + Region + + GLOBAL + + + + + + + {/* Action Buttons */} + + + {isAdmin && ( + + )} + + + + + + + ); } diff --git a/apps/website/components/teams/TeamGrid.tsx b/apps/website/components/teams/TeamGrid.tsx index 1021a543d..a49cb13ca 100644 --- a/apps/website/components/teams/TeamGrid.tsx +++ b/apps/website/components/teams/TeamGrid.tsx @@ -1,5 +1,7 @@ +'use client'; + +import React, { ReactNode } from 'react'; import { Grid } from '@/ui/Grid'; -import { ReactNode } from 'react'; interface TeamGridProps { children: ReactNode; @@ -7,7 +9,10 @@ interface TeamGridProps { export function TeamGrid({ children }: TeamGridProps) { return ( - + {children} ); diff --git a/apps/website/components/teams/TeamMembersTable.tsx b/apps/website/components/teams/TeamMembersTable.tsx index 08a9e1a4b..114db8145 100644 --- a/apps/website/components/teams/TeamMembersTable.tsx +++ b/apps/website/components/teams/TeamMembersTable.tsx @@ -1,9 +1,12 @@ 'use client'; +import React from 'react'; import { Button } from '@/ui/Button'; import { Stack } from '@/ui/Stack'; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/ui/Table'; +import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from '@/ui/Table'; import { Text } from '@/ui/Text'; +import { Box } from '@/ui/Box'; +import { Surface } from '@/ui/Surface'; interface Member { driverId: string; @@ -21,40 +24,54 @@ interface TeamMembersTableProps { export function TeamMembersTable({ members, isAdmin, onRemoveMember }: TeamMembersTableProps) { return ( - + - - Personnel - Role - Joined - Rating - {isAdmin && Actions} - + Personnel + Role + Joined + Rating + {isAdmin && Actions} {members.map((member) => ( - - - {member.driverName.substring(0, 2).toUpperCase()} - - {member.driverName} + + + {member.driverName.substring(0, 2).toUpperCase()} + + {member.driverName} - - {member.role} - + + {member.role} + - + {member.joinedAtLabel} - 1450 + 1450 {isAdmin && ( @@ -73,6 +90,6 @@ export function TeamMembersTable({ members, isAdmin, onRemoveMember }: TeamMembe ))}
-
+ ); } diff --git a/apps/website/components/teams/TeamSearchBar.tsx b/apps/website/components/teams/TeamSearchBar.tsx index 8b3bb0d52..6861c84fb 100644 --- a/apps/website/components/teams/TeamSearchBar.tsx +++ b/apps/website/components/teams/TeamSearchBar.tsx @@ -1,10 +1,9 @@ - - 'use client'; import { Icon } from '@/ui/Icon'; import { Input } from '@/ui/Input'; import { Search } from 'lucide-react'; +import { Box } from '@/ui/Box'; import React from 'react'; interface TeamSearchBarProps { @@ -14,13 +13,16 @@ interface TeamSearchBarProps { export function TeamSearchBar({ searchQuery, onSearchChange }: TeamSearchBarProps) { return ( - onSearchChange(e.target.value)} - icon={} - fullWidth - /> + + onSearchChange(e.target.value)} + icon={} + variant="search" + fullWidth + /> + ); } diff --git a/apps/website/components/teams/TeamStandingsPanel.tsx b/apps/website/components/teams/TeamStandingsPanel.tsx index 8fe9e3d58..b4ed419f8 100644 --- a/apps/website/components/teams/TeamStandingsPanel.tsx +++ b/apps/website/components/teams/TeamStandingsPanel.tsx @@ -1,8 +1,10 @@ 'use client'; +import React from 'react'; import { Box } from '@/ui/Box'; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/ui/Table'; +import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from '@/ui/Table'; import { Text } from '@/ui/Text'; +import { Surface } from '@/ui/Surface'; interface Standing { leagueId: string; @@ -18,9 +20,9 @@ interface TeamStandingsPanelProps { export function TeamStandingsPanel({ standings }: TeamStandingsPanelProps) { return ( - - - + + + Active Campaign Standings @@ -28,51 +30,49 @@ export function TeamStandingsPanel({ standings }: TeamStandingsPanelProps) { {standings.length > 0 ? ( - - League - Pos - Races - Points - + League + Pos + Races + Points {standings.map((s) => ( - {s.leagueName} + {s.leagueName} - + {s.position} - {s.races} + {s.races} - {s.points} + {s.points} ))}
) : ( - - + + No active campaign telemetry )} - +
); } diff --git a/apps/website/components/teams/TeamsDirectory.tsx b/apps/website/components/teams/TeamsDirectory.tsx index 4cf3ab7f3..f30db474c 100644 --- a/apps/website/components/teams/TeamsDirectory.tsx +++ b/apps/website/components/teams/TeamsDirectory.tsx @@ -1,46 +1,37 @@ 'use client'; -import { ReactNode } from 'react'; +import React, { ReactNode } from 'react'; +import { Box } from '@/ui/Box'; import { Container } from '@/ui/Container'; -import { Group } from '@/ui/Group'; -import { Text } from '@/ui/Text'; -import { StatusDot } from '@/ui/StatusDot'; +import { Heading } from '@/ui/Heading'; interface TeamsDirectoryProps { children: ReactNode; - title?: string; - subtitle?: string; } -export function TeamsDirectory({ children, title, subtitle }: TeamsDirectoryProps) { +export function TeamsDirectory({ children }: TeamsDirectoryProps) { return ( - - - {title && ( - - - {title} - - )} + + {children} - - + +
); } -export function TeamsDirectorySection({ children, title, accentColor = "primary-accent" }: { children: ReactNode, title: string, accentColor?: string }) { - const intentMap: Record = { - 'primary-accent': 'primary', - 'telemetry-aqua': 'telemetry', - }; +interface TeamsDirectorySectionProps { + title: string; + children: ReactNode; + accentColor?: string; +} +export function TeamsDirectorySection({ title, children }: TeamsDirectorySectionProps) { return ( - - - - {title} - + + + {title} + {children} - +
); } diff --git a/apps/website/components/teams/TeamsDirectoryHeader.tsx b/apps/website/components/teams/TeamsDirectoryHeader.tsx index ba9d2f9ad..1e349b144 100644 --- a/apps/website/components/teams/TeamsDirectoryHeader.tsx +++ b/apps/website/components/teams/TeamsDirectoryHeader.tsx @@ -1,27 +1,26 @@ 'use client'; - + +import React from 'react'; +import { Plus } from 'lucide-react'; import { Button } from '@/ui/Button'; -import { Icon } from '@/ui/Icon'; -import { PageHeader } from '@/ui/PageHeader'; -import { Plus, Users } from 'lucide-react'; - +import { TeamsHeader } from '@/ui/TeamsHeader'; + interface TeamsDirectoryHeaderProps { onCreateTeam: () => void; } - + export function TeamsDirectoryHeader({ onCreateTeam }: TeamsDirectoryHeaderProps) { return ( - } + icon={} > - Initialize Team + Register Team } /> diff --git a/apps/website/lib/builders/view-data/TeamsViewDataBuilder.ts b/apps/website/lib/builders/view-data/TeamsViewDataBuilder.ts index e0c046232..58db39806 100644 --- a/apps/website/lib/builders/view-data/TeamsViewDataBuilder.ts +++ b/apps/website/lib/builders/view-data/TeamsViewDataBuilder.ts @@ -19,6 +19,11 @@ export class TeamsViewDataBuilder { ratingLabel: RatingDisplay.format(team.rating), winsLabel: NumberDisplay.format(team.totalWins || 0), racesLabel: NumberDisplay.format(team.totalRaces || 0), + region: team.region, + isRecruiting: team.isRecruiting, + category: team.category, + performanceLevel: team.performanceLevel, + description: team.description, })); return { teams }; diff --git a/apps/website/lib/view-data/TeamsViewData.ts b/apps/website/lib/view-data/TeamsViewData.ts index f7ee0da64..15ec733b9 100644 --- a/apps/website/lib/view-data/TeamsViewData.ts +++ b/apps/website/lib/view-data/TeamsViewData.ts @@ -14,6 +14,11 @@ export interface TeamSummaryData { ratingLabel: string; winsLabel: string; racesLabel: string; + region?: string; + isRecruiting: boolean; + category?: string; + performanceLevel?: string; + description?: string; } export interface TeamsViewData extends ViewData { diff --git a/apps/website/templates/TeamDetailTemplate.tsx b/apps/website/templates/TeamDetailTemplate.tsx index e4a3bd247..b9a993221 100644 --- a/apps/website/templates/TeamDetailTemplate.tsx +++ b/apps/website/templates/TeamDetailTemplate.tsx @@ -1,23 +1,20 @@ 'use client'; -import { SlotTemplates } from '@/components/sponsors/SlotTemplates'; -import { SponsorInsightsCard } from '@/components/sponsors/SponsorInsightsCard'; -import { useSponsorMode } from '@/hooks/sponsor/useSponsorMode'; +import React from 'react'; +import { useRouter } from 'next/navigation'; import { Box } from '@/ui/Box'; -import { Breadcrumbs } from '@/ui/Breadcrumbs'; -import { Button } from '@/ui/Button'; import { Container } from '@/ui/Container'; import { Heading } from '@/ui/Heading'; -import { HorizontalStatItem } from '@/ui/HorizontalStatItem'; -import { Grid } from '@/ui/Grid'; -import { GridItem } from '@/ui/GridItem'; -import { Stack } from '@/ui/Stack'; import { Text } from '@/ui/Text'; - -import { TeamAdmin } from '@/components/teams/TeamAdmin'; +import { Stack } from '@/ui/Stack'; +import { Grid } from '@/ui/Grid'; +import { Button } from '@/ui/Button'; +import { Panel } from '@/ui/Panel'; +import { Breadcrumbs } from '@/ui/Breadcrumbs'; import { TeamDetailsHeader } from '@/components/teams/TeamDetailsHeader'; import { TeamMembersTable } from '@/components/teams/TeamMembersTable'; import { TeamStandingsPanel } from '@/components/teams/TeamStandingsPanel'; +import { TeamAdmin } from '@/components/teams/TeamAdmin'; import type { TeamDetailViewData } from '@/lib/view-data/TeamDetailViewData'; type Tab = 'overview' | 'roster' | 'standings' | 'admin'; @@ -26,8 +23,6 @@ export interface TeamDetailTemplateProps { viewData: TeamDetailViewData; activeTab: Tab; loading: boolean; - - // Event handlers onTabChange: (tab: Tab) => void; onUpdate: () => void; onRemoveMember: (driverId: string) => void; @@ -43,15 +38,14 @@ export function TeamDetailTemplate({ onRemoveMember, onGoBack, }: TeamDetailTemplateProps) { - const isSponsorMode = useSponsorMode(); const team = viewData.team; if (loading) { return ( - - - - Synchronizing Telemetry... + + + + Synchronizing Telemetry... ); @@ -59,28 +53,30 @@ export function TeamDetailTemplate({ if (!team) { return ( - - - - 404: Team Disconnected - - The requested team entity is no longer broadcasting. - - - - - - + + + + + + 404: Team Disconnected + + The requested team entity is no longer broadcasting. + + + + + + ); } return ( - - - + + + - {isSponsorMode && ( - window.location.href = href} - /> - )} - onTabChange('admin')} /> {/* Tabs */} - - + + {viewData.tabs.map((tab) => ( tab.visible && ( onTabChange(tab.id)} - pb={4} + onClick={() => onTabChange(tab.id as Tab)} + paddingBottom={4} cursor="pointer" position="relative" > @@ -132,8 +112,7 @@ export function TeamDetailTemplate({ weight="bold" size="xs" uppercase - letterSpacing="widest" - color={activeTab === tab.id ? 'text-primary-accent' : 'text-gray-500'} + variant={activeTab === tab.id ? 'primary' : 'low'} > {tab.label} @@ -141,10 +120,10 @@ export function TeamDetailTemplate({ )} @@ -155,58 +134,73 @@ export function TeamDetailTemplate({ {activeTab === 'overview' && ( - - - - - - Mission Statement - + + + + + Mission Statement + + {team.description || 'No description provided.'} + - - Recent Operations - - NO RECENT TELEMETRY DATA + + + Recent Operations + + + + NO RECENT TELEMETRY DATA - - - + + + + - - - - Performance Metrics - - - - - + + + + + Performance Metrics - - -
-
+ + + Avg Rating + 1450 + + + Win Rate + 12.5% + + + Total Podiums + 42 + + + + + + )} {activeTab === 'roster' && ( - - - Active Personnel - {viewData.memberships.length} UNITS ACTIVE - + + + Active Personnel + {viewData.memberships.length} UNITS ACTIVE + - + )} {activeTab === 'standings' && ( - // Mocked for now as in original + )} {activeTab === 'admin' && viewData.isAdmin && ( diff --git a/apps/website/templates/TeamsTemplate.tsx b/apps/website/templates/TeamsTemplate.tsx index 045d6a5d7..511124b63 100644 --- a/apps/website/templates/TeamsTemplate.tsx +++ b/apps/website/templates/TeamsTemplate.tsx @@ -1,67 +1,119 @@ 'use client'; -import { TeamCard } from '@/components/teams/TeamCardWrapper'; -import { TeamGrid } from '@/components/teams/TeamGrid'; -import { TeamLeaderboardPreview } from '@/components/teams/TeamLeaderboardPreviewWrapper'; -import { TeamsDirectoryHeader } from '@/components/teams/TeamsDirectoryHeader'; -import { TeamsDirectory, TeamsDirectorySection } from '@/components/teams/TeamsDirectory'; -import { EmptyState } from '@/ui/EmptyState'; -import type { TeamsViewData } from '@/lib/view-data/TeamsViewData'; +import React, { useMemo } from 'react'; import { Users } from 'lucide-react'; import { TemplateProps } from '@/lib/contracts/components/ComponentContracts'; -import React from 'react'; +import { TeamsViewData, TeamSummaryData } from '@/lib/view-data/TeamsViewData'; +import { TeamsDirectoryHeader } from '@/components/teams/TeamsDirectoryHeader'; +import { TeamGrid } from '@/components/teams/TeamGrid'; +import { TeamCard } from '@/components/teams/TeamCard'; +import { TeamSearchBar } from '@/components/teams/TeamSearchBar'; +import { EmptyState } from '@/ui/EmptyState'; +import { Container } from '@/ui/Container'; +import { Heading } from '@/ui/Heading'; +import { Text } from '@/ui/Text'; interface TeamsTemplateProps extends TemplateProps { + searchQuery: string; + onSearchChange: (query: string) => void; onTeamClick?: (teamId: string) => void; onViewFullLeaderboard: () => void; onCreateTeam: () => void; } -export function TeamsTemplate({ viewData, onTeamClick, onViewFullLeaderboard, onCreateTeam }: TeamsTemplateProps) { +export function TeamsTemplate({ + viewData, + searchQuery, + onSearchChange, + onTeamClick, + onViewFullLeaderboard, + onCreateTeam +}: TeamsTemplateProps) { const { teams } = viewData; + const filteredTeams = useMemo(() => { + return teams.filter(team => + team.teamName.toLowerCase().includes(searchQuery.toLowerCase()) || + (team.leagueName && team.leagueName.toLowerCase().includes(searchQuery.toLowerCase())) || + (team.region && team.region.toLowerCase().includes(searchQuery.toLowerCase())) + ); + }, [teams, searchQuery]); + + const clusters = useMemo(() => { + if (searchQuery) { + return [{ title: 'Search Results', teams: filteredTeams }]; + } + + const topTeams = [...teams] + .sort((a, b) => parseFloat(b.ratingLabel) - parseFloat(a.ratingLabel)) + .slice(0, 3); + + const recruitingTeams = teams.filter(t => t.isRecruiting && !topTeams.find(top => top.teamId === t.teamId)); + + const otherTeams = teams.filter(t => + !topTeams.find(top => top.teamId === t.teamId) && + !recruitingTeams.find(r => r.teamId === t.teamId) + ); + + const result = []; + if (topTeams.length > 0) result.push({ title: 'Top Rated Teams', teams: topTeams }); + if (recruitingTeams.length > 0) result.push({ title: 'Recruiting Now', teams: recruitingTeams }); + if (otherTeams.length > 0) result.push({ title: 'Active Rosters', teams: otherTeams }); + + return result; + }, [teams, filteredTeams, searchQuery]); + return ( - - - - - {teams.length > 0 ? ( - - {teams.map((team) => ( - onTeamClick?.(team.teamId)} - /> - ))} - - ) : ( - - )} - - - - onTeamClick?.(id)} - onViewFullLeaderboard={onViewFullLeaderboard} +
+ + + + - - + + {clusters.length > 0 ? ( +
+ {clusters.map((cluster) => ( +
+
+ + {cluster.title} + +
+
+ {cluster.teams.length} +
+
+ + + {cluster.teams.map((team) => ( + onTeamClick?.(id)} + /> + ))} + +
+ ))} +
+ ) : ( +
+ +
+ )} + +
); } diff --git a/apps/website/ui/Box.tsx b/apps/website/ui/Box.tsx index 219fc659b..bc74d9b06 100644 --- a/apps/website/ui/Box.tsx +++ b/apps/website/ui/Box.tsx @@ -12,7 +12,7 @@ import React, { forwardRef, ForwardedRef, ElementType } from 'react'; * If you need more complex behavior, create a specific component in apps/website/components. */ -export type Spacing = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96; +export type Spacing = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96 | string; interface ResponsiveSpacing { base?: Spacing; @@ -82,7 +82,7 @@ export interface BoxProps { h?: string | number | ResponsiveValue; // Display - display?: 'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none' | string | ResponsiveValue<'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none' | string>; + display?: 'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none' | string | ResponsiveValue<'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none' | string | any>; center?: boolean; overflow?: 'auto' | 'hidden' | 'visible' | 'scroll' | string; overflowX?: 'auto' | 'hidden' | 'visible' | 'scroll'; @@ -393,7 +393,13 @@ export const Box = forwardRef(( 5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: '10', 11: '11', 12: '12', 14: '14', 16: '16', 20: '20', 24: '24', 28: '28', 32: '32', 36: '36', 40: '40', 44: '44', 48: '48', 52: '52', 56: '56', 60: '60', 64: '64', 72: '72', 80: '80', 96: '96', - 'auto': 'auto' + 'auto': 'auto', + 'none': '0', + 'xs': '2', + 'sm': '4', + 'md': '6', + 'lg': '8', + 'xl': '12' }; const getSpacingClass = (prefix: string, value: Spacing | 'auto' | ResponsiveSpacing | undefined) => { diff --git a/apps/website/ui/Button.tsx b/apps/website/ui/Button.tsx index fb33d195c..94ed42e55 100644 --- a/apps/website/ui/Button.tsx +++ b/apps/website/ui/Button.tsx @@ -41,6 +41,12 @@ export interface ButtonProps { borderWidth?: string | any; aspectRatio?: string | any; border?: boolean | any; + ring?: string | any; + overflow?: string | any; + display?: string | any; + transform?: string | any; + hoverScale?: boolean | any; + minHeight?: string | any; } /** diff --git a/apps/website/ui/Card.tsx b/apps/website/ui/Card.tsx index 167f2822a..2f084f83b 100644 --- a/apps/website/ui/Card.tsx +++ b/apps/website/ui/Card.tsx @@ -1,12 +1,13 @@ import React, { ReactNode, forwardRef } from 'react'; import { Heading } from './Heading'; +import { Spacing } from './Box'; export interface CardProps { children: ReactNode; - variant?: 'default' | 'muted' | 'outline' | 'glass' | 'dark' | any; + variant?: 'default' | 'muted' | 'outline' | 'glass' | 'dark' | 'precision' | 'bordered' | 'elevated' | 'rarity-common' | 'rarity-rare' | 'rarity-epic' | 'rarity-legendary'; title?: string | ReactNode; footer?: ReactNode; - padding?: 'none' | 'sm' | 'md' | 'lg' | number | any; + padding?: Spacing | number | any; className?: string; style?: React.CSSProperties; bg?: string; @@ -28,6 +29,9 @@ export interface CardProps { gap?: number; py?: number; backgroundColor?: string; + group?: boolean | any; + w?: string | any; + justifyContent?: string | any; } /** @@ -68,6 +72,13 @@ export const Card = forwardRef(({ outline: 'bg-transparent border-[var(--ui-color-border-default)]', glass: 'bg-white/[0.03] backdrop-blur-md border-white/[0.05]', dark: 'bg-[var(--ui-color-bg-base)] border-[var(--ui-color-border-default)]', + precision: 'bg-[var(--ui-color-bg-surface)] border-[var(--ui-color-border-default)] shadow-[inset_0_1px_0_0_rgba(255,255,255,0.02)]', + bordered: 'bg-[var(--ui-color-bg-surface)] border-[var(--ui-color-border-default)]', + elevated: 'bg-[var(--ui-color-bg-surface)] border-[var(--ui-color-border-default)] shadow-md', + 'rarity-common': 'bg-gray-500/10 border-gray-500/50', + 'rarity-rare': 'bg-blue-500/10 border-blue-500/50', + 'rarity-epic': 'bg-purple-500/10 border-purple-500/50', + 'rarity-legendary': 'bg-orange-500/10 border-orange-500/50', }; const paddingClasses = { @@ -78,7 +89,7 @@ export const Card = forwardRef(({ }; const getPaddingClass = (pad: any) => { - if (typeof pad === 'string') return paddingClasses[pad as keyof typeof paddingClasses] || paddingClasses.md; + if (typeof pad === 'string') return `p-${pad}`; return ''; // Handled in style }; @@ -86,8 +97,8 @@ export const Card = forwardRef(({ ...style, ...(bg ? { backgroundColor: bg.startsWith('bg-') ? undefined : bg } : {}), ...(backgroundColor ? { backgroundColor } : {}), - ...(p !== undefined ? { padding: `${p * 0.25}rem` } : {}), - ...(py !== undefined ? { paddingTop: `${py * 0.25}rem`, paddingBottom: `${py * 0.25}rem` } : {}), + ...(p !== undefined ? { padding: typeof p === 'number' ? `${p * 0.25}rem` : undefined } : {}), + ...(py !== undefined ? { paddingTop: typeof py === 'number' ? `${py * 0.25}rem` : undefined, paddingBottom: typeof py === 'number' ? `${py * 0.25}rem` : undefined } : {}), ...(typeof padding === 'number' ? { padding: `${padding * 0.25}rem` } : {}), ...(responsiveColSpan?.lg ? { gridColumn: `span ${responsiveColSpan.lg} / span ${responsiveColSpan.lg}` } : {}), ...(overflow ? { overflow } : {}), diff --git a/apps/website/ui/Heading.tsx b/apps/website/ui/Heading.tsx index 6e629a731..31f49ae3b 100644 --- a/apps/website/ui/Heading.tsx +++ b/apps/website/ui/Heading.tsx @@ -1,4 +1,4 @@ -import { ReactNode, forwardRef } from 'react'; +import React, { ReactNode, forwardRef, CSSProperties } from 'react'; export interface HeadingProps { children: ReactNode; @@ -8,7 +8,7 @@ export interface HeadingProps { uppercase?: boolean; intent?: 'primary' | 'telemetry' | 'warning' | 'critical' | 'default' | any; className?: string; - style?: React.CSSProperties; + style?: CSSProperties; mb?: number | any; marginBottom?: number | any; mt?: number | any; @@ -19,6 +19,10 @@ export interface HeadingProps { truncate?: boolean; size?: string; icon?: ReactNode; + id?: string; + lineHeight?: string | any; + groupHoverColor?: string | any; + transition?: boolean | any; } /** diff --git a/apps/website/ui/Panel.tsx b/apps/website/ui/Panel.tsx index 2779dfc6a..134220d7d 100644 --- a/apps/website/ui/Panel.tsx +++ b/apps/website/ui/Panel.tsx @@ -1,94 +1,100 @@ -import { ReactNode } from 'react'; -import { Heading } from './Heading'; -import { Text } from './Text'; +import React, { ReactNode, CSSProperties } from 'react'; +import { Spacing } from './Box'; export interface PanelProps { - title?: string; - description?: string; children: ReactNode; - footer?: ReactNode; - variant?: 'default' | 'muted' | 'ghost' | 'dark' | any; - padding?: 'none' | 'sm' | 'md' | 'lg' | number | any; + variant?: 'default' | 'muted' | 'outline' | 'glass' | 'dark' | 'precision' | 'bordered' | 'elevated'; + padding?: Spacing | number; + onClick?: () => void; + style?: CSSProperties; + title?: string | ReactNode; actions?: ReactNode; - className?: string; - style?: React.CSSProperties; + description?: string; + footer?: ReactNode; border?: boolean; rounded?: string; + className?: string; borderColor?: string; bg?: string; } -/** - * Panel - Redesigned for "Modern Precision" theme. - * Includes compatibility props to prevent app-wide breakage. - */ -export const Panel = ({ - title, - description, +export function Panel({ children, - footer, - variant = 'default', + variant = 'default', padding = 'md', - actions, - className, + onClick, style, + title, + actions, + description, + footer, border, rounded, - borderColor, - bg, -}: PanelProps) => { + className +}: PanelProps) { const variantClasses = { - default: 'bg-[var(--ui-color-bg-surface)] border-[var(--ui-color-border-default)]', - muted: 'bg-[var(--ui-color-bg-surface-muted)] border-[var(--ui-color-border-muted)]', - ghost: 'bg-transparent border-transparent', - dark: 'bg-[var(--ui-color-bg-base)] border-[var(--ui-color-border-default)]', + default: 'bg-[var(--ui-color-bg-surface)] border border-[var(--ui-color-border-default)] shadow-sm', + muted: 'bg-[var(--ui-color-bg-surface-muted)] border border-[var(--ui-color-border-muted)]', + outline: 'bg-transparent border border-[var(--ui-color-border-default)]', + glass: 'bg-white/[0.03] backdrop-blur-md border border-white/[0.05]', + dark: 'bg-[var(--ui-color-bg-base)] border border-[var(--ui-color-border-default)]', + precision: 'bg-[var(--ui-color-bg-surface)] border border-[var(--ui-color-border-default)] shadow-[inset_0_1px_0_0_rgba(255,255,255,0.02)]', + bordered: 'bg-[var(--ui-color-bg-surface)] border border-[var(--ui-color-border-default)]', + elevated: 'bg-[var(--ui-color-bg-surface)] border border-[var(--ui-color-border-default)] shadow-md', }; const paddingClasses = { none: 'p-0', - sm: 'p-2', - md: 'p-4', - lg: 'p-8', + xs: 'p-2', + sm: 'p-4', + md: 'p-6', + lg: 'p-10', }; - const getPaddingClass = (pad: any) => { - if (typeof pad === 'string') return paddingClasses[pad as keyof typeof paddingClasses] || paddingClasses.md; - return ''; // Handled in style + const getPaddingClass = (p: any) => { + if (typeof p === 'string') return `p-${p}`; + return ''; }; - const combinedStyle: React.CSSProperties = { - ...style, - ...(bg ? { backgroundColor: bg.startsWith('bg-') ? undefined : bg } : {}), - ...(typeof padding === 'number' ? { padding: `${padding * 0.25}rem` } : {}), - ...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor } : {}), - ...(border === false ? { border: 'none' } : {}), - }; + const interactiveClasses = onClick + ? 'cursor-pointer hover:border-[var(--ui-color-border-bright)] transition-all duration-200 ease-in-out active:scale-[0.99]' + : ''; return ( -
- {(title || description || actions) && ( -
-
- {title && {title}} - {description && {description}} -
- {actions && ( -
- {actions} +
+ {(title || actions) && ( +
+ {title && ( +
+
+
+

+ {title} +

+
+ {description && ( +

+ {description} +

+ )}
)} + {actions &&
{actions}
}
)} - -
- {children} -
- + {children} {footer && ( -
+
{footer}
)}
); -}; +} diff --git a/apps/website/ui/Surface.tsx b/apps/website/ui/Surface.tsx index db027c858..612df9819 100644 --- a/apps/website/ui/Surface.tsx +++ b/apps/website/ui/Surface.tsx @@ -17,8 +17,8 @@ export interface SurfaceProps extends BoxProps as?: T; children?: ReactNode; variant?: 'default' | 'dark' | 'muted' | 'glass' | 'discord' | 'gradient-blue' | 'gradient-gold' | 'gradient-purple' | 'gradient-green' | 'discord-inner' | 'outline' | 'rarity-common' | 'rarity-rare' | 'rarity-epic' | 'rarity-legendary' | 'precision'; - rounded?: keyof ThemeRadii | 'none' | '2xl'; - shadow?: keyof ThemeShadows | 'none'; + rounded?: keyof ThemeRadii | 'none' | '2xl' | string | boolean; + shadow?: keyof ThemeShadows | 'none' | string; } export const Surface = forwardRef(( diff --git a/apps/website/ui/Table.tsx b/apps/website/ui/Table.tsx index cb3cd6d1a..b6e3f18ee 100644 --- a/apps/website/ui/Table.tsx +++ b/apps/website/ui/Table.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, CSSProperties } from 'react'; import { Surface } from './Surface'; export interface TableProps { @@ -16,7 +16,14 @@ export const Table = ({ children, className }: TableProps) => { ); }; -export const TableHeader = ({ children, className, textAlign, w }: { children: ReactNode, className?: string, textAlign?: 'left' | 'center' | 'right', w?: string }) => { +export interface TableHeaderProps { + children: ReactNode; + className?: string; + textAlign?: 'left' | 'center' | 'right'; + w?: string; +} + +export const TableHeader = ({ children, className, textAlign, w }: TableHeaderProps) => { return ( @@ -55,7 +62,14 @@ export const TableRow = ({ children, onClick, className, variant, clickable, bg, ); }; -export const TableHeaderCell = ({ children, textAlign, w, className }: { children: ReactNode, textAlign?: 'left' | 'center' | 'right', w?: string, className?: string }) => { +export interface TableHeaderCellProps { + children: ReactNode; + textAlign?: 'left' | 'center' | 'right'; + w?: string; + className?: string; +} + +export const TableHeaderCell = ({ children, textAlign, w, className }: TableHeaderCellProps) => { const alignClass = textAlign === 'center' ? 'text-center' : (textAlign === 'right' ? 'text-right' : 'text-left'); return ( { +export interface TableCellProps { + children: ReactNode; + textAlign?: 'left' | 'center' | 'right'; + className?: string; + py?: number; + colSpan?: number; + w?: string; + position?: string; + [key: string]: any; +} + +export const TableCell = ({ children, textAlign, className, py, colSpan, w, position, ...props }: TableCellProps) => { const alignClass = textAlign === 'center' ? 'text-center' : (textAlign === 'right' ? 'text-right' : 'text-left'); return ( void; + description?: string; } export const TeamCard = ({ name, - description, + leagueName, logo, memberCount, - isRecruiting, - badges, - region, + rating, + wins, + races, + region = 'EU', + isRecruiting, + performanceLevel, + description, onClick }: TeamCardProps) => { return ( - - - {logo} - - - - {name} - {isRecruiting && RECRUITING} - - - {badges} - - - - - - {description || 'No description available'} - - - {region && ( - - -
- - {region} +
+ {/* Header: Logo and Identity */} +
+
+ {logo || } +
+
+
+ {name} + {isRecruiting && ( + RECRUITING + )}
- - - )} +
+ {leagueName && {leagueName}} + {performanceLevel && ( +
+ + {performanceLevel} +
+ )} +
+
+
- - - - - {memberCount} {memberCount === 1 ? 'MEMBER' : 'MEMBERS'} + {/* Technical Stats Grid - Engineered Look */} + {(rating || wins || races) && ( +
+
+ Rating + {rating || '-'} +
+
+ Wins + {wins || '-'} +
+
+ Races + {races || '-'} +
+
+ )} + + {description && ( + + {description} -
- - VIEW - - -
+ )} + + {/* Footer: Metadata */} +
+
+
+ + {memberCount} +
+
+ + {region} +
+
+ +
+ Details + +
+
+
); }; diff --git a/apps/website/ui/TeamsHeader.tsx b/apps/website/ui/TeamsHeader.tsx new file mode 100644 index 000000000..ad61086d4 --- /dev/null +++ b/apps/website/ui/TeamsHeader.tsx @@ -0,0 +1,33 @@ +import React, { ReactNode } from 'react'; +import { Heading } from './Heading'; +import { Text } from './Text'; + +interface TeamsHeaderProps { + title: string; + subtitle?: string; + action?: ReactNode; +} + +export function TeamsHeader({ title, subtitle, action }: TeamsHeaderProps) { + return ( +
+
+
+
+ {title} +
+ {subtitle && ( + + {subtitle} + + )} +
+ + {action && ( +
+ {action} +
+ )} +
+ ); +} diff --git a/apps/website/ui/Text.tsx b/apps/website/ui/Text.tsx index 319b8d6e2..2ede0f207 100644 --- a/apps/website/ui/Text.tsx +++ b/apps/website/ui/Text.tsx @@ -1,4 +1,5 @@ -import React, { ElementType, ReactNode, forwardRef } from 'react'; +import React, { ElementType, ReactNode, forwardRef, CSSProperties } from 'react'; +import { ResponsiveValue } from './Box'; export type TextSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | 'base'; @@ -15,7 +16,7 @@ export interface TextProps { block?: boolean; truncate?: boolean; className?: string; - style?: React.CSSProperties; + style?: CSSProperties; mt?: number | any; mb?: number | any; ml?: number | any; @@ -29,7 +30,7 @@ export interface TextProps { flexGrow?: number; flexShrink?: number; lineClamp?: number; - display?: string; + display?: string | ResponsiveValue; opacity?: number; maxWidth?: string | number; mx?: string | number; @@ -170,6 +171,20 @@ export const Text = forwardRef(({ loose: 'leading-loose', }; + const getResponsiveClasses = (prefix: string, value: any | ResponsiveValue | undefined) => { + if (value === undefined) return ''; + if (typeof value === 'object') { + const classes = []; + if (value.base !== undefined) classes.push(prefix ? `${prefix}-${value.base}` : String(value.base)); + if (value.sm !== undefined) classes.push(prefix ? `sm:${prefix}-${value.sm}` : `sm:${value.sm}`); + if (value.md !== undefined) classes.push(prefix ? `md:${prefix}-${value.md}` : `md:${value.md}`); + if (value.lg !== undefined) classes.push(prefix ? `lg:${prefix}-${value.lg}` : `lg:${value.lg}`); + if (value.xl !== undefined) classes.push(prefix ? `xl:${prefix}-${value.xl}` : `xl:${value.xl}`); + return classes.join(' '); + } + return prefix ? `${prefix}-${value}` : String(value); + }; + const classes = [ variantClasses[variant as keyof typeof variantClasses] || '', getResponsiveSize(size), @@ -180,6 +195,7 @@ export const Text = forwardRef(({ leadingClasses[leading as keyof typeof leadingClasses] || '', block ? 'block' : 'inline', truncate ? 'truncate' : '', + getResponsiveClasses('display', display), transition ? 'transition-all duration-200' : '', italic ? 'italic' : '', animate === 'pulse' ? 'animate-pulse' : '', @@ -189,7 +205,7 @@ export const Text = forwardRef(({ const combinedStyle: React.CSSProperties = { ...style, - ...(display ? { display } : {}), + ...(typeof display === 'string' ? { display } : {}), ...(alignItems ? { alignItems } : {}), ...(gap !== undefined ? { gap: `${gap * 0.25}rem` } : {}), ...(cursor ? { cursor } : {}), @@ -219,7 +235,7 @@ export const Text = forwardRef(({ ...(transform ? { textTransform: transform as any } : {}), }; - const Tag = as; + const Tag = as || 'p'; return (