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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,12 @@
'use client';
import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading';
import { Image } from '@/ui/Image';
import { Stack } from '@/ui/Stack';
import { TeamHero } from '@/ui/TeamHero';
import { Text } from '@/ui/Text';
import { Badge } from '@/ui/Badge';
import { StatGrid } from '@/ui/StatGrid';
interface TeamDetailsHeaderProps {
teamId: string;
name: string;
@@ -17,7 +18,7 @@ interface TeamDetailsHeaderProps {
isAdmin?: boolean;
onAdminClick?: () => void;
}
export function TeamDetailsHeader({
name,
tag,
@@ -29,84 +30,51 @@ export function TeamDetailsHeader({
onAdminClick,
}: TeamDetailsHeaderProps) {
return (
<Stack
bg="surface-charcoal"
border
borderColor="outline-steel"
p={8}
position="relative"
overflow="hidden"
>
{/* Background accent */}
<Stack
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"
>
<TeamHero
title={
<div className="flex items-center gap-3">
{name}
{tag && <Badge variant="outline">[{tag}]</Badge>}
</div>
}
description={description || 'No mission statement provided.'}
sideContent={
<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">
{logoUrl ? (
<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>
<Stack flex="1">
<Stack direction="row" align="center" gap={3}>
<Heading level={1} weight="bold">{name}</Heading>
{tag && (
<Stack px={2} py={1} bg="base-black" border borderColor="outline-steel">
<Text size="xs" font="mono" color="primary-accent" weight="bold">[{tag}]</Text>
</Stack>
)}
</Stack>
<Text color="text-gray-400" mt={2} block maxWidth="2xl">
{description || 'No mission statement provided.'}
</Text>
<Stack direction="row" gap={6} mt={6}>
<Stack>
<Text size="xs" color="text-gray-500" uppercase font="mono" letterSpacing="widest">Personnel</Text>
<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}>
</div>
}
stats={
<StatGrid
columns={2}
variant="box"
stats={[
{
label: 'Personnel',
value: `${memberCount} Units`,
},
{
label: 'Established',
value: foundedDate ? new Date(foundedDate).toLocaleDateString() : 'Unknown',
}
]}
/>
}
actions={
<>
{isAdmin && (
<Button variant="secondary" size="sm" onClick={onAdminClick}>
<Button variant="secondary" onClick={onAdminClick}>
Configure
</Button>
)}
<Button variant="primary" size="sm">
<Button variant="primary">
Join Request
</Button>
</Stack>
</Stack>
</Stack>
</>
}
/>
);
}

View File

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

View File

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

View File

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

View File

@@ -1,52 +1,30 @@
import { Box } from '@/ui/Box';
import { Icon } from '@/ui/Icon';
import { Image } from '@/ui/Image';
import { Logo } from '@/ui/Logo';
import { Users } from 'lucide-react';
export interface TeamLogoProps {
teamId?: string;
src?: string;
alt: string;
size?: number;
className?: string;
border?: boolean;
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
}
export function TeamLogo({
teamId,
src,
alt,
size = 48,
className = '',
border = true,
rounded = 'md',
}: TeamLogoProps) {
const logoSrc = src || (teamId ? `/media/teams/${teamId}/logo` : undefined);
return (
<Box
display="flex"
alignItems="center"
justifyContent="center"
<Logo
src={logoSrc}
alt={alt}
size={size}
rounded={rounded}
overflow="hidden"
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>
icon={Users}
/>
);
}

View File

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

View File

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

View File

@@ -1,42 +1,21 @@
'use client';
import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Plus } from 'lucide-react';
import { PageHeader } from '@/ui/PageHeader';
import { Plus, Users } from 'lucide-react';
interface TeamsDirectoryHeaderProps {
onCreateTeam: () => void;
}
export function TeamsDirectoryHeader({ onCreateTeam }: TeamsDirectoryHeaderProps) {
return (
<Stack
direction="row"
align="end"
justify="between"
wrap
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>
<PageHeader
icon={Users}
title="Teams"
description="Operational Units & Racing Collectives"
action={
<Button
variant="primary"
onClick={onCreateTeam}
@@ -44,7 +23,7 @@ export function TeamsDirectoryHeader({ onCreateTeam }: TeamsDirectoryHeaderProps
>
Initialize Team
</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;
value: string | number;
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?: {
value: number;
isPositive: boolean;
@@ -22,17 +24,19 @@ export const StatCard = ({
value,
icon,
intent = 'primary',
variant = 'default',
font = 'sans',
trend,
footer
}: StatCardProps) => {
return (
<Card variant="default">
<Card variant={variant}>
<Box display="flex" alignItems="start" justifyContent="between" marginBottom={4}>
<Box>
<Text size="xs" weight="bold" variant="low" uppercase>
{label}
</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}
</Text>
</Box>

View File

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