website refactor

This commit is contained in:
2026-01-18 23:36:04 +01:00
parent 182056a57b
commit 7c1cf62d4e
16 changed files with 317 additions and 373 deletions

View File

@@ -1,7 +1,4 @@
import { Grid } from '@/ui/Grid'; import { StatGrid } from '@/ui/StatGrid';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Card } from '@/ui/Card';
interface KpiItem { interface KpiItem {
label: string; label: string;
@@ -20,29 +17,16 @@ interface DashboardKpiRowProps {
*/ */
export function DashboardKpiRow({ items }: DashboardKpiRowProps) { export function DashboardKpiRow({ items }: DashboardKpiRowProps) {
return ( return (
<Grid cols={{ base: 2, md: 3, lg: 6 }} gap={4}> <StatGrid
{items.map((item, index) => ( variant="card"
<Card key={index} variant="dark"> cardVariant="dark"
<Stack gap={1}> font="mono"
<Text columns={{ base: 2, md: 3, lg: 6 }}
size="xs" stats={items.map(item => ({
weight="bold" label: item.label,
uppercase value: item.value,
variant="low" intent: item.intent as any
> }))}
{item.label} />
</Text>
<Text
size="xl"
font="mono"
weight="bold"
variant={item.intent || 'high'}
>
{item.value}
</Text>
</Stack>
</Card>
))}
</Grid>
); );
} }

View File

@@ -4,7 +4,7 @@ import { useCreateTeam } from "@/hooks/team/useCreateTeam";
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { InfoBanner } from '@/ui/InfoBanner'; import { InfoBanner } from '@/ui/InfoBanner';
import { Input } from '@/ui/Input'; import { Input } from '@/ui/Input';
import { Stack } from '@/ui/Stack'; import { Group } from '@/ui/Group';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { TextArea } from '@/ui/TextArea'; import { TextArea } from '@/ui/TextArea';
import React, { useState } from 'react'; import React, { useState } from 'react';
@@ -79,8 +79,8 @@ export function CreateTeamForm({ onCancel, onSuccess, onNavigate }: CreateTeamFo
}; };
return ( return (
<Stack as="form" onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<Stack gap={6}> <Group direction="col" align="stretch" gap={6}>
<Input <Input
label="Team Name *" label="Team Name *"
type="text" type="text"
@@ -88,8 +88,7 @@ export function CreateTeamForm({ onCancel, onSuccess, onNavigate }: CreateTeamFo
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Enter team name..." placeholder="Enter team name..."
disabled={createTeamMutation.isPending} disabled={createTeamMutation.isPending}
variant={errors.name ? 'error' : 'default'} error={errors.name}
errorMessage={errors.name}
/> />
<Input <Input
@@ -100,8 +99,7 @@ export function CreateTeamForm({ onCancel, onSuccess, onNavigate }: CreateTeamFo
placeholder="e.g., APEX" placeholder="e.g., APEX"
maxLength={4} maxLength={4}
disabled={createTeamMutation.isPending} disabled={createTeamMutation.isPending}
variant={errors.tag ? 'error' : 'default'} error={errors.tag}
errorMessage={errors.tag}
/> />
<TextArea <TextArea
@@ -111,20 +109,19 @@ export function CreateTeamForm({ onCancel, onSuccess, onNavigate }: CreateTeamFo
onChange={(e) => setFormData({ ...formData, description: e.target.value })} onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Describe your team's goals and racing style..." placeholder="Describe your team's goals and racing style..."
disabled={createTeamMutation.isPending} disabled={createTeamMutation.isPending}
variant={errors.description ? 'error' : 'default'} error={errors.description}
errorMessage={errors.description}
/> />
<InfoBanner title="About Team Creation"> <InfoBanner title="About Team Creation">
<Stack as="ul" gap={1}> <Group direction="col" align="start" gap={1}>
<Text as="li" size="sm" color="text-gray-400"> You will be assigned as the team owner</Text> <Text size="sm" variant="low"> You will be assigned as the team owner</Text>
<Text as="li" size="sm" color="text-gray-400"> You can invite other drivers to join your team</Text> <Text size="sm" variant="low"> You can invite other drivers to join your team</Text>
<Text as="li" size="sm" color="text-gray-400"> Team standings are calculated across leagues</Text> <Text size="sm" variant="low"> Team standings are calculated across leagues</Text>
<Text as="li" size="sm" color="text-gray-400"> This is alpha data - it resets on page reload</Text> <Text size="sm" variant="low"> This is alpha data - it resets on page reload</Text>
</Stack> </Group>
</InfoBanner> </InfoBanner>
<Stack display="flex" gap={3}> <Group gap={3} fullWidth>
<Button <Button
type="submit" type="submit"
variant="primary" variant="primary"
@@ -143,8 +140,8 @@ export function CreateTeamForm({ onCancel, onSuccess, onNavigate }: CreateTeamFo
Cancel Cancel
</Button> </Button>
)} )}
</Stack> </Group>
</Stack> </Group>
</Stack> </form>
); );
} }

View File

@@ -1,7 +1,6 @@
'use client'; 'use client';
import { Image } from '@/ui/Image'; import { TeamLogo } from '@/components/teams/TeamLogo';
import { PlaceholderImage } from '@/ui/PlaceholderImage';
import { TeamCard as UITeamCard } from '@/ui/TeamCard'; import { TeamCard as UITeamCard } from '@/ui/TeamCard';
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
@@ -42,17 +41,7 @@ export function TeamCard({
region={region} region={region}
onClick={onClick} onClick={onClick}
logo={ logo={
logo ? ( <TeamLogo src={logo} alt={name} size={64} />
<Image
src={logo}
alt={name}
fullWidth
fullHeight
objectFit="cover"
/>
) : (
<PlaceholderImage />
)
} }
badges={ badges={
<> <>

View File

@@ -2,8 +2,9 @@ import { TeamCard as UiTeamCard } from '@/components/teams/TeamCard';
import { TeamStatItem } from '@/components/teams/TeamStatItem'; import { TeamStatItem } from '@/components/teams/TeamStatItem';
import { Badge } from '@/ui/Badge'; import { Badge } from '@/ui/Badge';
import { Icon } from '@/ui/Icon'; import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack'; import { Group } from '@/ui/Group';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { BadgeGroup } from '@/ui/BadgeGroup';
import { import {
Clock, Clock,
Crown, Crown,
@@ -40,7 +41,7 @@ function getPerformanceBadge(level?: string) {
case 'advanced': case 'advanced':
return { icon: Star, label: 'Advanced', variant: 'primary' as const }; return { icon: Star, label: 'Advanced', variant: 'primary' as const };
case 'intermediate': case 'intermediate':
return { icon: TrendingUp, label: 'Intermediate', variant: 'info' as const }; return { icon: TrendingUp, label: 'Intermediate', variant: 'default' as const };
case 'beginner': case 'beginner':
return { icon: Shield, label: 'Beginner', variant: 'success' as const }; return { icon: Shield, label: 'Beginner', variant: 'success' as const };
default: default:
@@ -51,9 +52,9 @@ function getPerformanceBadge(level?: string) {
function getSpecializationBadge(specialization?: string) { function getSpecializationBadge(specialization?: string) {
switch (specialization) { switch (specialization) {
case 'endurance': case 'endurance':
return { icon: Clock, label: 'Endurance', color: 'var(--warning-amber)' }; return { icon: Clock, label: 'Endurance', intent: 'warning' as const };
case 'sprint': case 'sprint':
return { icon: Zap, label: 'Sprint', color: 'var(--neon-aqua)' }; return { icon: Zap, label: 'Sprint', intent: 'telemetry' as const };
default: default:
return null; return null;
} }
@@ -93,42 +94,28 @@ export function TeamCard({
</Badge> </Badge>
)} )}
specializationContent={specializationBadge && ( specializationContent={specializationBadge && (
<Stack direction="row" align="center" gap={1}> <Group gap={1}>
<Icon icon={specializationBadge.icon} size={3} color={specializationBadge.color} /> <Icon icon={specializationBadge.icon} size={3} intent={specializationBadge.intent} />
<Text size="xs" color="text-gray-500">{specializationBadge.label}</Text> <Text size="xs" variant="low">{specializationBadge.label}</Text>
</Stack> </Group>
)} )}
categoryBadge={category && ( categoryBadge={category && (
<Badge variant="primary"> <Badge variant="primary">
<Stack w="2" h="2" rounded="full" bg="bg-purple-500" mr={1.5} />
{category} {category}
</Badge> </Badge>
)} )}
languagesContent={languages && languages.length > 0 && ( languagesContent={languages && languages.length > 0 && (
<Stack <Badge variant="default" icon={Languages}>
display="flex" {languages.slice(0, 2).join(', ')}
alignItems="center" {languages.length > 2 && ` +${languages.length - 2}`}
gap={1.5} </Badge>
px={2}
py={1}
rounded="md"
bg="bg-iron-gray/50"
border
borderColor="border-charcoal-outline/30"
>
<Icon icon={Languages} size={3} color="var(--neon-purple)" />
<Text size="xs" color="text-gray-400">
{languages.slice(0, 2).join(', ')}
{languages.length > 2 && ` +${languages.length - 2}`}
</Text>
</Stack>
)} )}
statsContent={ statsContent={
<> <Group gap={4} justify="center">
<TeamStatItem label="Rating" value={typeof rating === 'number' ? Math.round(rating).toLocaleString() : '—'} color="text-primary-blue" align="center" /> <TeamStatItem label="Rating" value={typeof rating === 'number' ? Math.round(rating).toLocaleString() : '—'} intent="primary" align="center" />
<TeamStatItem label="Wins" value={totalWins ?? 0} color="text-performance-green" align="center" /> <TeamStatItem label="Wins" value={totalWins ?? 0} intent="success" align="center" />
<TeamStatItem label="Races" value={totalRaces ?? 0} color="text-white" align="center" /> <TeamStatItem label="Races" value={totalRaces ?? 0} intent="high" align="center" />
</> </Group>
} }
/> />
); );

View File

@@ -1,10 +1,11 @@
'use client'; 'use client';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading';
import { Image } from '@/ui/Image'; import { Image } from '@/ui/Image';
import { Stack } from '@/ui/Stack'; import { TeamHero } from '@/ui/TeamHero';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { Badge } from '@/ui/Badge';
import { StatGrid } from '@/ui/StatGrid';
interface TeamDetailsHeaderProps { interface TeamDetailsHeaderProps {
teamId: string; teamId: string;
@@ -29,84 +30,51 @@ export function TeamDetailsHeader({
onAdminClick, onAdminClick,
}: TeamDetailsHeaderProps) { }: TeamDetailsHeaderProps) {
return ( return (
<Stack <TeamHero
bg="surface-charcoal" title={
border <div className="flex items-center gap-3">
borderColor="outline-steel" {name}
p={8} {tag && <Badge variant="outline">[{tag}]</Badge>}
position="relative" </div>
overflow="hidden" }
> description={description || 'No mission statement provided.'}
{/* Background accent */} sideContent={
<Stack <div className="w-32 h-32 bg-[var(--ui-color-bg-surface-muted)] border border-[var(--ui-color-border-default)] flex items-center justify-center overflow-hidden rounded-lg">
position="absolute"
top="0"
right="0"
w="64"
h="64"
bg="primary-accent/5"
rounded="full"
blur="3xl"
translate="-1/2, -1/2"
/>
<Stack direction="row" align="start" gap={8} position="relative">
<Stack
w="32"
h="32"
bg="base-black"
border
borderColor="outline-steel"
display="flex"
center
overflow="hidden"
>
{logoUrl ? ( {logoUrl ? (
<Image src={logoUrl} alt={name} width={128} height={128} /> <Image src={logoUrl} alt={name} width={128} height={128} />
) : ( ) : (
<Text size="2xl" weight="bold" color="text-gray-700">{name.substring(0, 2).toUpperCase()}</Text> <Text size="2xl" weight="bold" variant="low">{name.substring(0, 2).toUpperCase()}</Text>
)} )}
</Stack> </div>
}
<Stack flex="1"> stats={
<Stack direction="row" align="center" gap={3}> <StatGrid
<Heading level={1} weight="bold">{name}</Heading> columns={2}
{tag && ( variant="box"
<Stack px={2} py={1} bg="base-black" border borderColor="outline-steel"> stats={[
<Text size="xs" font="mono" color="primary-accent" weight="bold">[{tag}]</Text> {
</Stack> label: 'Personnel',
)} value: `${memberCount} Units`,
</Stack> },
{
<Text color="text-gray-400" mt={2} block maxWidth="2xl"> label: 'Established',
{description || 'No mission statement provided.'} value: foundedDate ? new Date(foundedDate).toLocaleDateString() : 'Unknown',
</Text> }
]}
<Stack direction="row" gap={6} mt={6}> />
<Stack> }
<Text size="xs" color="text-gray-500" uppercase font="mono" letterSpacing="widest">Personnel</Text> actions={
<Text block weight="bold" color="text-white">{memberCount} Units</Text> <>
</Stack>
<Stack>
<Text size="xs" color="text-gray-500" uppercase font="mono" letterSpacing="widest">Established</Text>
<Text block weight="bold" color="text-white">
{foundedDate ? new Date(foundedDate).toLocaleDateString() : 'Unknown'}
</Text>
</Stack>
</Stack>
</Stack>
<Stack gap={3}>
{isAdmin && ( {isAdmin && (
<Button variant="secondary" size="sm" onClick={onAdminClick}> <Button variant="secondary" onClick={onAdminClick}>
Configure Configure
</Button> </Button>
)} )}
<Button variant="primary" size="sm"> <Button variant="primary">
Join Request Join Request
</Button> </Button>
</Stack> </>
</Stack> }
</Stack> />
); );
} }

View File

@@ -7,7 +7,7 @@ import { Input } from '@/ui/Input';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { ControlBar } from '@/ui/ControlBar'; import { ControlBar } from '@/ui/ControlBar';
import { SegmentedControl } from '@/ui/SegmentedControl'; import { SegmentedControl } from '@/ui/SegmentedControl';
import { Stack } from '@/ui/Stack'; import { Group } from '@/ui/Group';
import { ButtonGroup } from '@/ui/ButtonGroup'; import { ButtonGroup } from '@/ui/ButtonGroup';
import { Hash, LucideIcon, Percent, Search, Star, Trophy } from 'lucide-react'; import { Hash, LucideIcon, Percent, Search, Star, Trophy } from 'lucide-react';
import React from 'react'; import React from 'react';
@@ -18,11 +18,11 @@ type SortBy = 'rating' | 'wins' | 'winRate' | 'races';
const SKILL_LEVELS: { const SKILL_LEVELS: {
id: SkillLevel; id: SkillLevel;
label: string; label: string;
variant: 'warning' | 'primary' | 'info' | 'success'; variant: 'warning' | 'primary' | 'default' | 'success';
}[] = [ }[] = [
{ id: 'pro', label: 'Pro', variant: 'warning' }, { id: 'pro', label: 'Pro', variant: 'warning' },
{ id: 'advanced', label: 'Advanced', variant: 'primary' }, { id: 'advanced', label: 'Advanced', variant: 'primary' },
{ id: 'intermediate', label: 'Intermediate', variant: 'info' }, { id: 'intermediate', label: 'Intermediate', variant: 'default' },
{ id: 'beginner', label: 'Beginner', variant: 'success' }, { id: 'beginner', label: 'Beginner', variant: 'success' },
]; ];
@@ -51,7 +51,7 @@ export function TeamFilter({
onSortChange, onSortChange,
}: TeamFilterProps) { }: TeamFilterProps) {
return ( return (
<Stack gap={4} marginBottom={6}> <Group direction="col" align="stretch" gap={4} fullWidth>
<ControlBar <ControlBar
leftContent={ leftContent={
<Input <Input
@@ -92,7 +92,7 @@ export function TeamFilter({
</ButtonGroup> </ButtonGroup>
</ControlBar> </ControlBar>
<Stack align="center" gap={2}> <Group align="center" gap={2}>
<Text size="sm" variant="low">Sort by:</Text> <Text size="sm" variant="low">Sort by:</Text>
<SegmentedControl <SegmentedControl
options={SORT_OPTIONS.map(opt => ({ options={SORT_OPTIONS.map(opt => ({
@@ -103,7 +103,7 @@ export function TeamFilter({
activeId={sortBy} activeId={sortBy}
onChange={(id) => onSortChange(id as SortBy)} onChange={(id) => onSortChange(id as SortBy)}
/> />
</Stack> </Group>
</Stack> </Group>
); );
} }

View File

@@ -1,11 +1,12 @@
'use client';
import { JoinTeamButton } from '@/components/teams/JoinTeamButton'; import { JoinTeamButton } from '@/components/teams/JoinTeamButton';
import { TeamLogo } from '@/components/teams/TeamLogo'; import { TeamLogo } from '@/components/teams/TeamLogo';
import { TeamTag } from '@/components/teams/TeamTag'; import { TeamTag } from '@/components/teams/TeamTag';
import { Card } from '@/ui/Card'; import { Card } from '@/ui/Card';
import { Group } from '@/ui/Group';
import { Heading } from '@/ui/Heading'; import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/Stack'; import { StatGrid } from '@/ui/StatGrid';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
interface TeamHeroProps { interface TeamHeroProps {
@@ -25,54 +26,46 @@ interface TeamHeroProps {
export function TeamHero({ team, memberCount, onUpdate }: TeamHeroProps) { export function TeamHero({ team, memberCount, onUpdate }: TeamHeroProps) {
return ( return (
<Card> <Card>
<Stack direction="row" align="start" justify="between" wrap gap={6}> <Group align="start" justify="between" wrap gap={6}>
<Stack direction="row" align="start" gap={6} wrap flexGrow={1}> <Group align="start" gap={6} wrap fullWidth>
<Stack <TeamLogo teamId={team.id} alt={team.name} size={96} />
w="24"
h="24"
rounded="lg"
p={1}
overflow="hidden"
bg="bg-deep-graphite"
display="flex"
alignItems="center"
justifyContent="center"
>
<TeamLogo teamId={team.id} alt={team.name} />
</Stack>
<Stack flexGrow={1} minWidth="0"> <Group direction="col" align="start" gap={2} fullWidth>
<Stack direction="row" align="center" gap={3} mb={2}> <Group gap={3}>
<Heading level={1}>{team.name}</Heading> <Heading level={1}>{team.name}</Heading>
{team.tag && <TeamTag tag={team.tag} />} {team.tag && <TeamTag tag={team.tag} />}
</Stack> </Group>
<Text color="text-gray-300" block mb={4} maxWidth="42rem">{team.description}</Text> <Text variant="low" block marginBottom={4}>{team.description}</Text>
<Stack direction="row" align="center" gap={4} wrap> <StatGrid
<Text size="sm" color="text-gray-400">{memberCount} {memberCount === 1 ? 'member' : 'members'}</Text> columns={{ base: 2, md: 4 }}
{team.category && ( variant="box"
<Stack direction="row" align="center" gap={1.5}> stats={[
<Stack w="2" h="2" rounded="full" bg="bg-purple-500" /> {
<Text size="sm" color="text-purple-400">{team.category}</Text> label: 'Personnel',
</Stack> value: `${memberCount} ${memberCount === 1 ? 'member' : 'members'}`,
)} },
{team.createdAt && ( ...(team.category ? [{
<Text size="sm" color="text-gray-400"> label: 'Category',
Founded {new Date(team.createdAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })} value: team.category,
</Text> intent: 'primary' as const,
)} }] : []),
{team.leagues && team.leagues.length > 0 && ( ...(team.createdAt ? [{
<Text size="sm" color="text-gray-400"> label: 'Founded',
Active in {team.leagues.length} {team.leagues.length === 1 ? 'league' : 'leagues'} value: new Date(team.createdAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' }),
</Text> }] : []),
)} ...(team.leagues && team.leagues.length > 0 ? [{
</Stack> label: 'Activity',
</Stack> value: `${team.leagues.length} ${team.leagues.length === 1 ? 'league' : 'leagues'}`,
</Stack> }] : []),
]}
/>
</Group>
</Group>
<JoinTeamButton teamId={team.id} onUpdate={onUpdate} /> <JoinTeamButton teamId={team.id} onUpdate={onUpdate} />
</Stack> </Group>
</Card> </Card>
); );
} }

View File

@@ -1,6 +1,7 @@
import { Image } from '@/ui/Image'; import { TeamLogo } from '@/components/teams/TeamLogo';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { BadgeGroup } from '@/ui/BadgeGroup';
import { Group } from '@/ui/Group';
interface TeamIdentityProps { interface TeamIdentityProps {
name: string; name: string;
@@ -11,34 +12,21 @@ interface TeamIdentityProps {
export function TeamIdentity({ name, logoUrl, performanceLevel, category }: TeamIdentityProps) { export function TeamIdentity({ name, logoUrl, performanceLevel, category }: TeamIdentityProps) {
return ( return (
<Stack direction="row" align="center" gap={3}> <Group gap={3}>
<Stack width="10" height="10" rounded="lg" overflow="hidden" border borderColor="border-charcoal-outline"> <TeamLogo src={logoUrl} alt={name} size={40} />
<Image <Group direction="col" align="start" gap={1} fullWidth>
src={logoUrl} <Text weight="semibold" variant="high" block truncate>{name}</Text>
alt={name}
width={40}
height={40}
fullWidth
fullHeight
objectFit="cover"
/>
</Stack>
<Stack flex={1}>
<Text weight="semibold" color="text-white" block truncate>{name}</Text>
{(performanceLevel || category) && ( {(performanceLevel || category) && (
<Stack direction="row" align="center" gap={2} mt={1} wrap> <BadgeGroup>
{performanceLevel && ( {performanceLevel && (
<Text size="xs" color="text-gray-500">{performanceLevel}</Text> <Text size="xs" variant="low">{performanceLevel}</Text>
)} )}
{category && ( {category && (
<Stack direction="row" align="center" gap={1}> <Text size="xs" variant="primary">{category}</Text>
<Stack width="1.5" height="1.5" rounded="full" bg="bg-primary-blue" opacity={0.5} />
<Text size="xs" color="text-primary-blue">{category}</Text>
</Stack>
)} )}
</Stack> </BadgeGroup>
)} )}
</Stack> </Group>
</Stack> </Group>
); );
} }

View File

@@ -1,6 +1,4 @@
import { Box } from '@/ui/Box'; import { Logo } from '@/ui/Logo';
import { Icon } from '@/ui/Icon';
import { Image } from '@/ui/Image';
import { Users } from 'lucide-react'; import { Users } from 'lucide-react';
export interface TeamLogoProps { export interface TeamLogoProps {
@@ -8,8 +6,6 @@ export interface TeamLogoProps {
src?: string; src?: string;
alt: string; alt: string;
size?: number; size?: number;
className?: string;
border?: boolean;
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full'; rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
} }
@@ -18,35 +14,17 @@ export function TeamLogo({
src, src,
alt, alt,
size = 48, size = 48,
className = '',
border = true,
rounded = 'md', rounded = 'md',
}: TeamLogoProps) { }: TeamLogoProps) {
const logoSrc = src || (teamId ? `/media/teams/${teamId}/logo` : undefined); const logoSrc = src || (teamId ? `/media/teams/${teamId}/logo` : undefined);
return ( return (
<Box <Logo
display="flex" src={logoSrc}
alignItems="center" alt={alt}
justifyContent="center" size={size}
rounded={rounded} rounded={rounded}
overflow="hidden" icon={Users}
bg="bg-charcoal-outline/10" />
border={border}
borderColor="border-charcoal-outline/50"
className={className}
style={{ width: size, height: size, flexShrink: 0 }}
>
{logoSrc ? (
<Image
src={logoSrc}
alt={alt}
className="w-full h-full object-contain p-1"
fallbackSrc="/default-team-logo.png"
/>
) : (
<Icon icon={Users} size={size > 32 ? 5 : 4} color="text-gray-500" />
)}
</Box>
); );
} }

View File

@@ -3,8 +3,8 @@ import { TeamIdentity } from '@/components/teams/TeamIdentity';
import { getMediaUrl } from '@/lib/utilities/media'; import { getMediaUrl } from '@/lib/utilities/media';
import { Card } from '@/ui/Card'; import { Card } from '@/ui/Card';
import { Icon } from '@/ui/Icon'; import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack'; import { Group } from '@/ui/Group';
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 { Text } from '@/ui/Text';
import { Users } from 'lucide-react'; import { Users } from 'lucide-react';
@@ -30,27 +30,15 @@ interface TeamRankingsTableProps {
export function TeamRankingsTable({ teams, sortBy, onTeamClick }: TeamRankingsTableProps) { export function TeamRankingsTable({ teams, sortBy, onTeamClick }: TeamRankingsTableProps) {
return ( return (
<Card p={0} overflow="hidden"> <Card padding={0}>
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableHeader> <TableHeaderCell textAlign="center">Rank</TableHeaderCell>
<Text size="xs" weight="medium" color="text-gray-500" align="center" block>Rank</Text> <TableHeaderCell>Team</TableHeaderCell>
</TableHeader> <TableHeaderCell textAlign="center">Members</TableHeaderCell>
<TableHeader> <TableHeaderCell textAlign="center">Rating</TableHeaderCell>
<Text size="xs" weight="medium" color="text-gray-500" block>Team</Text> <TableHeaderCell textAlign="center">Wins</TableHeaderCell>
</TableHeader>
<TableHeader>
<Stack display={{ base: 'none', lg: 'block' }}>
<Text size="xs" weight="medium" color="text-gray-500" align="center" block>Members</Text>
</Stack>
</TableHeader>
<TableHeader>
<Text size="xs" weight="medium" color="text-gray-500" align="center" block>Rating</Text>
</TableHeader>
<TableHeader>
<Text size="xs" weight="medium" color="text-gray-500" align="center" block>Wins</Text>
</TableHeader>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
@@ -60,7 +48,7 @@ export function TeamRankingsTable({ teams, sortBy, onTeamClick }: TeamRankingsTa
onClick={() => onTeamClick(team.id)} onClick={() => onTeamClick(team.id)}
clickable clickable
> >
<TableCell> <TableCell textAlign="center">
<RankBadge rank={index + 1} /> <RankBadge rank={index + 1} />
</TableCell> </TableCell>
<TableCell> <TableCell>
@@ -71,27 +59,21 @@ export function TeamRankingsTable({ teams, sortBy, onTeamClick }: TeamRankingsTa
category={team.category} category={team.category}
/> />
</TableCell> </TableCell>
<TableCell> <TableCell textAlign="center">
<Stack display={{ base: 'none', lg: 'flex' }} alignItems="center" justifyContent="center"> <Group justify="center" gap={1.5}>
<Stack direction="row" align="center" gap={1.5}> <Icon icon={Users} size={3.5} intent="low" />
<Icon icon={Users} size={3.5} color="text-gray-500" /> <Text size="sm" variant="low">{team.memberCount}</Text>
<Text size="sm" color="text-gray-400">{team.memberCount}</Text> </Group>
</Stack>
</Stack>
</TableCell> </TableCell>
<TableCell> <TableCell textAlign="center">
<Stack display="flex" center> <Text font="mono" weight="semibold" variant={sortBy === 'rating' ? 'primary' : 'high'}>
<Text font="mono" weight="semibold" color={sortBy === 'rating' ? 'text-primary-blue' : 'text-white'}> 0
0 </Text>
</Text>
</Stack>
</TableCell> </TableCell>
<TableCell> <TableCell textAlign="center">
<Stack display="flex" center> <Text font="mono" weight="semibold" variant={sortBy === 'wins' ? 'primary' : 'high'}>
<Text font="mono" weight="semibold" color={sortBy === 'wins' ? 'text-primary-blue' : 'text-white'}> {team.totalWins}
{team.totalWins} </Text>
</Text>
</Stack>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}

View File

@@ -1,25 +1,22 @@
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { Group } from '@/ui/Group';
interface TeamStatItemProps { interface TeamStatItemProps {
label: string; label: string;
value: string | number; value: string | number;
color?: string; intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low';
align?: 'left' | 'center' | 'right'; align?: 'left' | 'center' | 'right';
} }
export function TeamStatItem({ label, value, color = 'text-white', align = 'left' }: TeamStatItemProps) { export function TeamStatItem({ label, value, intent = 'high', align = 'left' }: TeamStatItemProps) {
return ( return (
<Box <Group
p={2} direction="col"
rounded="lg" align={align === 'center' ? 'center' : align === 'right' ? 'end' : 'start'}
bg="bg-iron-gray/30" gap={1}
display="flex"
flexDirection="col"
alignItems={align === 'center' ? 'center' : align === 'right' ? 'end' : 'start'}
> >
<Text size="xs" color="text-gray-500" block mb={0.5}>{label}</Text> <Text size="xs" variant="low" block>{label}</Text>
<Text size="sm" weight="semibold" color={color}>{value}</Text> <Text size="sm" weight="semibold" variant={intent}>{value}</Text>
</Box> </Group>
); );
} }

View File

@@ -1,11 +1,9 @@
'use client'; 'use client';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon'; import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack'; import { PageHeader } from '@/ui/PageHeader';
import { Text } from '@/ui/Text'; import { Plus, Users } from 'lucide-react';
import { Plus } from 'lucide-react';
interface TeamsDirectoryHeaderProps { interface TeamsDirectoryHeaderProps {
onCreateTeam: () => void; onCreateTeam: () => void;
@@ -13,30 +11,11 @@ interface TeamsDirectoryHeaderProps {
export function TeamsDirectoryHeader({ onCreateTeam }: TeamsDirectoryHeaderProps) { export function TeamsDirectoryHeader({ onCreateTeam }: TeamsDirectoryHeaderProps) {
return ( return (
<Stack <PageHeader
direction="row" icon={Users}
align="end" title="Teams"
justify="between" description="Operational Units & Racing Collectives"
wrap action={
gap={4}
borderBottom
borderColor="outline-steel"
pb={6}
>
<Stack>
<Heading level={1} weight="bold">Teams</Heading>
<Text
color="text-gray-500"
size="sm"
mt={1}
font="mono"
uppercase
letterSpacing="widest"
>
Operational Units & Racing Collectives
</Text>
</Stack>
<Stack>
<Button <Button
variant="primary" variant="primary"
onClick={onCreateTeam} onClick={onCreateTeam}
@@ -44,7 +23,7 @@ export function TeamsDirectoryHeader({ onCreateTeam }: TeamsDirectoryHeaderProps
> >
Initialize Team Initialize Team
</Button> </Button>
</Stack> }
</Stack> />
); );
} }

36
apps/website/ui/Group.tsx Normal file
View File

@@ -0,0 +1,36 @@
import React, { ReactNode } from 'react';
import { Box } from './Box';
export interface GroupProps {
children: ReactNode;
direction?: 'row' | 'col';
align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline';
justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
gap?: number;
wrap?: boolean;
fullWidth?: boolean;
}
export const Group = ({
children,
direction = 'row',
align = 'center',
justify = 'start',
gap = 3,
wrap = false,
fullWidth = false,
}: GroupProps) => {
return (
<Box
display="flex"
flexDirection={direction}
alignItems={align}
justifyContent={justify}
gap={gap}
flexWrap={wrap ? 'wrap' : 'nowrap'}
fullWidth={fullWidth}
>
{children}
</Box>
);
};

53
apps/website/ui/Logo.tsx Normal file
View File

@@ -0,0 +1,53 @@
import React from 'react';
import { Box } from './Box';
import { Surface } from './Surface';
import { Image } from './Image';
import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react';
export interface LogoProps {
src?: string;
alt: string;
size?: number | string;
icon?: LucideIcon;
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
variant?: 'default' | 'dark' | 'muted' | 'glass';
border?: boolean;
}
export const Logo = ({
src,
alt,
size = 48,
icon,
rounded = 'md',
variant = 'muted',
border = true,
}: LogoProps) => {
return (
<Surface
variant={variant}
rounded={rounded}
border={border}
style={{
width: size,
height: size,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
flexShrink: 0
}}
>
{src ? (
<Image
src={src}
alt={alt}
style={{ width: '100%', height: '100%', objectFit: 'contain', padding: '10%' }}
/>
) : icon ? (
<Icon icon={icon} size={typeof size === 'number' ? (size > 32 ? 5 : 4) : 5} intent="low" />
) : null}
</Surface>
);
};

View File

@@ -9,7 +9,9 @@ export interface StatCardProps {
label: string; label: string;
value: string | number; value: string | number;
icon?: LucideIcon; icon?: LucideIcon;
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry' | 'low'; intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry' | 'low' | 'high' | 'med';
variant?: 'default' | 'dark' | 'muted' | 'glass' | 'outline';
font?: 'sans' | 'mono';
trend?: { trend?: {
value: number; value: number;
isPositive: boolean; isPositive: boolean;
@@ -22,17 +24,19 @@ export const StatCard = ({
value, value,
icon, icon,
intent = 'primary', intent = 'primary',
variant = 'default',
font = 'sans',
trend, trend,
footer footer
}: StatCardProps) => { }: StatCardProps) => {
return ( return (
<Card variant="default"> <Card variant={variant}>
<Box display="flex" alignItems="start" justifyContent="between" marginBottom={4}> <Box display="flex" alignItems="start" justifyContent="between" marginBottom={4}>
<Box> <Box>
<Text size="xs" weight="bold" variant="low" uppercase> <Text size="xs" weight="bold" variant="low" uppercase>
{label} {label}
</Text> </Text>
<Text size="2xl" weight="bold" variant="high" block marginTop={1}> <Text size="2xl" weight="bold" variant={intent as any || 'high'} font={font} block marginTop={1}>
{value} {value}
</Text> </Text>
</Box> </Box>

View File

@@ -7,12 +7,16 @@ export interface StatGridProps {
stats: (StatBoxProps | StatCardProps)[]; stats: (StatBoxProps | StatCardProps)[];
columns?: number | { base?: number; sm?: number; md?: number; lg?: number; xl?: number }; columns?: number | { base?: number; sm?: number; md?: number; lg?: number; xl?: number };
variant?: 'box' | 'card'; variant?: 'box' | 'card';
cardVariant?: 'default' | 'dark' | 'muted' | 'glass' | 'outline';
font?: 'sans' | 'mono';
} }
export const StatGrid = ({ export const StatGrid = ({
stats, stats,
columns = 3, columns = 3,
variant = 'box' variant = 'box',
cardVariant,
font
}: StatGridProps) => { }: StatGridProps) => {
return ( return (
<Grid cols={columns} gap={4}> <Grid cols={columns} gap={4}>
@@ -20,7 +24,12 @@ export const StatGrid = ({
variant === 'box' ? ( variant === 'box' ? (
<StatBox key={index} {...(stat as StatBoxProps)} /> <StatBox key={index} {...(stat as StatBoxProps)} />
) : ( ) : (
<StatCard key={index} {...(stat as StatCardProps)} /> <StatCard
key={index}
variant={cardVariant}
font={font}
{...(stat as StatCardProps)}
/>
) )
))} ))}
</Grid> </Grid>