website refactor
This commit is contained in:
@@ -1,18 +1,15 @@
|
|||||||
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;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low';
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low';
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DashboardKpiRowProps {
|
interface DashboardKpiRowProps {
|
||||||
items: KpiItem[];
|
items: KpiItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DashboardKpiRow
|
* DashboardKpiRow
|
||||||
*
|
*
|
||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
'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';
|
||||||
|
|
||||||
interface TeamCardProps {
|
interface TeamCardProps {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -19,7 +18,7 @@ interface TeamCardProps {
|
|||||||
statsContent?: ReactNode;
|
statsContent?: ReactNode;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TeamCard({
|
export function TeamCard({
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
@@ -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={
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
'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;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -17,7 +18,7 @@ interface TeamDetailsHeaderProps {
|
|||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
onAdminClick?: () => void;
|
onAdminClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TeamDetailsHeader({
|
export function TeamDetailsHeader({
|
||||||
name,
|
name,
|
||||||
tag,
|
tag,
|
||||||
@@ -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>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,32 @@
|
|||||||
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;
|
||||||
logoUrl: string;
|
logoUrl: string;
|
||||||
performanceLevel?: string;
|
performanceLevel?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,30 @@
|
|||||||
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 {
|
||||||
teamId?: string;
|
teamId?: string;
|
||||||
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';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TeamLogo({
|
export function TeamLogo({
|
||||||
teamId,
|
teamId,
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,21 @@
|
|||||||
'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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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
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;
|
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user