website refactor
This commit is contained in:
@@ -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
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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={
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
36
apps/website/ui/Group.tsx
Normal 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
53
apps/website/ui/Logo.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user