website refactor
This commit is contained in:
83
apps/website/components/teams/TeamCard.tsx
Normal file
83
apps/website/components/teams/TeamCard.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Users, ChevronRight } from 'lucide-react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
import { Image } from '@/ui/Image';
|
||||
|
||||
interface TeamCardProps {
|
||||
id: string;
|
||||
name: string;
|
||||
logoUrl?: string;
|
||||
memberCount: number;
|
||||
isRecruiting?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export function TeamCard({ name, logoUrl, memberCount, isRecruiting, onClick }: TeamCardProps) {
|
||||
return (
|
||||
<Box
|
||||
as="article"
|
||||
bg="surface-charcoal"
|
||||
border
|
||||
borderColor="outline-steel"
|
||||
p={5}
|
||||
cursor="pointer"
|
||||
hover={{ borderColor: 'primary-accent', bg: 'surface-charcoal/80' }}
|
||||
transition="smooth"
|
||||
onClick={onClick}
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
>
|
||||
{/* Accent line */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
w="1"
|
||||
h="full"
|
||||
bg={isRecruiting ? 'telemetry-aqua' : 'outline-steel'}
|
||||
/>
|
||||
|
||||
<Stack direction="row" align="center" gap={4}>
|
||||
<Box
|
||||
w="12"
|
||||
h="12"
|
||||
bg="base-black"
|
||||
border
|
||||
borderColor="outline-steel"
|
||||
display="flex"
|
||||
center
|
||||
overflow="hidden"
|
||||
>
|
||||
{logoUrl ? (
|
||||
<Image src={logoUrl} alt={name} width={48} height={48} />
|
||||
) : (
|
||||
<Text size="xs" weight="bold" color="text-gray-600">{name.substring(0, 2).toUpperCase()}</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flex="1">
|
||||
<Heading level={4} weight="bold">{name}</Heading>
|
||||
<Stack direction="row" align="center" gap={3} mt={1}>
|
||||
<Stack direction="row" align="center" gap={1}>
|
||||
<Icon icon={Users} size={3} color="text-gray-500" />
|
||||
<Text size="xs" color="text-gray-500" font="mono">{memberCount}</Text>
|
||||
</Stack>
|
||||
{isRecruiting && (
|
||||
<Box px={1.5} py={0.5} bg="telemetry-aqua/10" border borderColor="telemetry-aqua/20">
|
||||
<Text size="xs" color="telemetry-aqua" weight="bold" uppercase letterSpacing="tighter">Recruiting</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Icon icon={ChevronRight} size={4} color="text-gray-700" />
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
114
apps/website/components/teams/TeamDetailsHeader.tsx
Normal file
114
apps/website/components/teams/TeamDetailsHeader.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Image } from '@/ui/Image';
|
||||
|
||||
interface TeamDetailsHeaderProps {
|
||||
teamId: string;
|
||||
name: string;
|
||||
tag?: string;
|
||||
description?: string;
|
||||
logoUrl?: string;
|
||||
memberCount: number;
|
||||
foundedDate?: string;
|
||||
isAdmin?: boolean;
|
||||
onAdminClick?: () => void;
|
||||
}
|
||||
|
||||
export function TeamDetailsHeader({
|
||||
name,
|
||||
tag,
|
||||
description,
|
||||
logoUrl,
|
||||
memberCount,
|
||||
foundedDate,
|
||||
isAdmin,
|
||||
onAdminClick,
|
||||
}: TeamDetailsHeaderProps) {
|
||||
return (
|
||||
<Box
|
||||
bg="surface-charcoal"
|
||||
border
|
||||
borderColor="outline-steel"
|
||||
p={8}
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
>
|
||||
{/* Background accent */}
|
||||
<Box
|
||||
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">
|
||||
<Box
|
||||
w="32"
|
||||
h="32"
|
||||
bg="base-black"
|
||||
border
|
||||
borderColor="outline-steel"
|
||||
display="flex"
|
||||
center
|
||||
overflow="hidden"
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flex="1">
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Heading level={1} weight="bold">{name}</Heading>
|
||||
{tag && (
|
||||
<Box px={2} py={1} bg="base-black" border borderColor="outline-steel">
|
||||
<Text size="xs" font="mono" color="primary-accent" weight="bold">[{tag}]</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Text color="text-gray-400" mt={2} block maxWidth="2xl">
|
||||
{description || 'No mission statement provided.'}
|
||||
</Text>
|
||||
|
||||
<Stack direction="row" gap={6} mt={6}>
|
||||
<Box>
|
||||
<Text size="xs" color="text-gray-500" uppercase font="mono" letterSpacing="widest">Personnel</Text>
|
||||
<Text block weight="bold" color="text-white">{memberCount} Units</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<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>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Stack gap={3}>
|
||||
{isAdmin && (
|
||||
<Button variant="secondary" size="sm" onClick={onAdminClick}>
|
||||
Configure
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="primary" size="sm">
|
||||
Join Request
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
29
apps/website/components/teams/TeamGrid.tsx
Normal file
29
apps/website/components/teams/TeamGrid.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { TeamCard } from './TeamCard';
|
||||
import type { TeamSummaryData } from '@/lib/view-data/TeamsViewData';
|
||||
|
||||
interface TeamGridProps {
|
||||
teams: TeamSummaryData[];
|
||||
onTeamClick?: (teamId: string) => void;
|
||||
}
|
||||
|
||||
export function TeamGrid({ teams, onTeamClick }: TeamGridProps) {
|
||||
return (
|
||||
<Grid cols={1} mdCols={2} lgCols={3} gap={6}>
|
||||
{teams.map((team) => (
|
||||
<TeamCard
|
||||
key={team.teamId}
|
||||
id={team.teamId}
|
||||
name={team.teamName}
|
||||
logoUrl={team.logoUrl}
|
||||
memberCount={team.memberCount}
|
||||
isRecruiting={true} // Redesign feel
|
||||
onClick={() => onTeamClick?.(team.teamId)}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -31,10 +31,8 @@ export function TeamHeroSection({
|
||||
py={12}
|
||||
px={8}
|
||||
rounded="2xl"
|
||||
style={{
|
||||
background: 'linear-gradient(to bottom right, rgba(147, 51, 234, 0.3), rgba(38, 38, 38, 0.8), #0f1115)',
|
||||
borderColor: 'rgba(147, 51, 234, 0.2)',
|
||||
}}
|
||||
bg="surface-charcoal"
|
||||
borderColor="outline-steel"
|
||||
border
|
||||
>
|
||||
{/* Background decorations */}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
import { TeamLeaderboardItem } from '@/ui/TeamLeaderboardItem';
|
||||
import { TeamLeaderboardPreview as UiTeamLeaderboardPreview } from '@/ui/TeamLeaderboardPreview';
|
||||
import { TeamLeaderboardPreview as SemanticTeamLeaderboardPreview } from '@/components/leaderboards/TeamLeaderboardPreview';
|
||||
|
||||
interface TeamLeaderboardPreviewProps {
|
||||
topTeams: Array<{
|
||||
@@ -24,58 +23,22 @@ export function TeamLeaderboardPreview({
|
||||
onTeamClick,
|
||||
onViewFullLeaderboard
|
||||
}: TeamLeaderboardPreviewProps) {
|
||||
const getMedalColor = (position: number) => {
|
||||
switch (position) {
|
||||
case 0: return '#facc15';
|
||||
case 1: return '#d1d5db';
|
||||
case 2: return '#d97706';
|
||||
default: return '#6b7280';
|
||||
}
|
||||
};
|
||||
|
||||
const getMedalBg = (position: number) => {
|
||||
switch (position) {
|
||||
case 0: return 'rgba(250, 204, 21, 0.1)';
|
||||
case 1: return 'rgba(209, 213, 219, 0.1)';
|
||||
case 2: return 'rgba(217, 119, 6, 0.1)';
|
||||
default: return 'rgba(38, 38, 38, 0.5)';
|
||||
}
|
||||
};
|
||||
|
||||
const getMedalBorder = (position: number) => {
|
||||
switch (position) {
|
||||
case 0: return 'rgba(250, 204, 21, 0.3)';
|
||||
case 1: return 'rgba(209, 213, 219, 0.3)';
|
||||
case 2: return 'rgba(217, 119, 6, 0.3)';
|
||||
default: return 'rgba(38, 38, 38, 1)';
|
||||
}
|
||||
};
|
||||
|
||||
if (topTeams.length === 0) return null;
|
||||
|
||||
return (
|
||||
<UiTeamLeaderboardPreview
|
||||
title="Top Teams"
|
||||
subtitle="Highest rated racing teams"
|
||||
onViewFull={onViewFullLeaderboard}
|
||||
>
|
||||
{topTeams.map((team, index) => (
|
||||
<TeamLeaderboardItem
|
||||
key={team.id}
|
||||
position={index + 1}
|
||||
name={team.name}
|
||||
logoUrl={team.logoUrl || getMediaUrl('team-logo', team.id)}
|
||||
category={team.category}
|
||||
memberCount={team.memberCount}
|
||||
totalWins={team.totalWins}
|
||||
isRecruiting={team.isRecruiting}
|
||||
rating={team.rating}
|
||||
onClick={() => onTeamClick(team.id)}
|
||||
medalColor={getMedalColor(index)}
|
||||
medalBg={getMedalBg(index)}
|
||||
medalBorder={getMedalBorder(index)}
|
||||
/>
|
||||
))}
|
||||
</UiTeamLeaderboardPreview>
|
||||
<SemanticTeamLeaderboardPreview
|
||||
teams={topTeams.map((team, index) => ({
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
tag: '', // Not available in this view data
|
||||
memberCount: team.memberCount,
|
||||
category: team.category,
|
||||
totalWins: team.totalWins,
|
||||
logoUrl: team.logoUrl || getMediaUrl('team-logo', team.id),
|
||||
position: index + 1
|
||||
}))}
|
||||
onTeamClick={onTeamClick}
|
||||
onNavigateToTeams={onViewFullLeaderboard}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
79
apps/website/components/teams/TeamMembersTable.tsx
Normal file
79
apps/website/components/teams/TeamMembersTable.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Table, TableBody, TableHead, TableHeader, TableRow, TableCell } from '@/ui/Table';
|
||||
|
||||
interface Member {
|
||||
driverId: string;
|
||||
driverName: string;
|
||||
role: string;
|
||||
joinedAt: string;
|
||||
}
|
||||
|
||||
interface TeamMembersTableProps {
|
||||
members: Member[];
|
||||
isAdmin?: boolean;
|
||||
onRemoveMember?: (driverId: string) => void;
|
||||
}
|
||||
|
||||
export function TeamMembersTable({ members, isAdmin, onRemoveMember }: TeamMembersTableProps) {
|
||||
return (
|
||||
<Box border borderColor="outline-steel" bg="surface-charcoal/30">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeader>Personnel</TableHeader>
|
||||
<TableHeader>Role</TableHeader>
|
||||
<TableHeader>Joined</TableHeader>
|
||||
<TableHeader textAlign="right">Rating</TableHeader>
|
||||
{isAdmin && <TableHeader textAlign="right">Actions</TableHeader>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{members.map((member) => (
|
||||
<TableRow key={member.driverId}>
|
||||
<TableCell>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Box w="8" h="8" bg="base-black" border borderColor="outline-steel" display="flex" center>
|
||||
<Text size="xs" weight="bold" color="primary-accent">{member.driverName.substring(0, 2).toUpperCase()}</Text>
|
||||
</Box>
|
||||
<Text weight="bold" size="sm" color="text-white">{member.driverName}</Text>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box px={2} py={0.5} bg="base-black" border borderColor="outline-steel" display="inline-block">
|
||||
<Text size="xs" color="text-gray-400" font="mono" uppercase>{member.role}</Text>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text size="xs" color="text-gray-500" font="mono">
|
||||
{new Date(member.joinedAt).toLocaleDateString()}
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell textAlign="right">
|
||||
<Text font="mono" weight="bold" color="primary-accent">1450</Text>
|
||||
</TableCell>
|
||||
{isAdmin && (
|
||||
<TableCell textAlign="right">
|
||||
{member.role !== 'owner' && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => onRemoveMember?.(member.driverId)}
|
||||
>
|
||||
DECOMMISSION
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -73,7 +73,7 @@ export function TeamPodium({ teams, onClick }: TeamPodiumProps) {
|
||||
h="auto"
|
||||
mb={4}
|
||||
p={0}
|
||||
className="transition-all"
|
||||
transition
|
||||
>
|
||||
<Box
|
||||
bg={getBgColor(position)}
|
||||
|
||||
79
apps/website/components/teams/TeamStandingsPanel.tsx
Normal file
79
apps/website/components/teams/TeamStandingsPanel.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Table, TableBody, TableHead, TableHeader, TableRow, TableCell } from '@/ui/Table';
|
||||
|
||||
interface Standing {
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
position: number;
|
||||
points: number;
|
||||
races: number;
|
||||
}
|
||||
|
||||
interface TeamStandingsPanelProps {
|
||||
standings: Standing[];
|
||||
}
|
||||
|
||||
export function TeamStandingsPanel({ standings }: TeamStandingsPanelProps) {
|
||||
return (
|
||||
<Box border borderColor="outline-steel" bg="surface-charcoal/30">
|
||||
<Box p={4} borderBottom borderColor="outline-steel">
|
||||
<Text size="xs" weight="bold" color="text-gray-400" uppercase letterSpacing="widest">
|
||||
Active Campaign Standings
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{standings.length > 0 ? (
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeader>League</TableHeader>
|
||||
<TableHeader textAlign="center">Pos</TableHeader>
|
||||
<TableHeader textAlign="center">Races</TableHeader>
|
||||
<TableHeader textAlign="right">Points</TableHeader>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{standings.map((s) => (
|
||||
<TableRow key={s.leagueId}>
|
||||
<TableCell>
|
||||
<Text weight="bold" size="sm" color="text-white">{s.leagueName}</Text>
|
||||
</TableCell>
|
||||
<TableCell textAlign="center">
|
||||
<Box
|
||||
display="inline-flex"
|
||||
center
|
||||
w="6"
|
||||
h="6"
|
||||
bg={s.position <= 3 ? 'warning-amber/20' : 'base-black'}
|
||||
border
|
||||
borderColor={s.position <= 3 ? 'warning-amber/40' : 'outline-steel'}
|
||||
>
|
||||
<Text size="xs" weight="bold" color={s.position <= 3 ? 'warning-amber' : 'text-gray-400'}>
|
||||
{s.position}
|
||||
</Text>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell textAlign="center">
|
||||
<Text size="xs" color="text-gray-500" font="mono">{s.races}</Text>
|
||||
</TableCell>
|
||||
<TableCell textAlign="right">
|
||||
<Text font="mono" weight="bold" color="telemetry-aqua">{s.points}</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<Box py={12} textAlign="center">
|
||||
<Text color="text-gray-600" font="mono" size="xs" uppercase letterSpacing="widest">
|
||||
No active campaign telemetry
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
52
apps/website/components/teams/TeamsDirectoryHeader.tsx
Normal file
52
apps/website/components/teams/TeamsDirectoryHeader.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Icon } from '@/ui/Icon';
|
||||
|
||||
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}
|
||||
>
|
||||
<Box>
|
||||
<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>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onCreateTeam}
|
||||
icon={<Icon icon={Plus} size={4} />}
|
||||
>
|
||||
Initialize Team
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user