website refactor
This commit is contained in:
@@ -12,6 +12,9 @@ import { Input } from '@/ui/Input';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { IconButton } from '@/ui/IconButton';
|
||||
import { useSidebar } from '@/components/layout/SidebarContext';
|
||||
import { PublicTopNav } from '@/ui/PublicTopNav';
|
||||
import { PublicNavLogin } from '@/ui/PublicNavLogin';
|
||||
import { PublicNavSignup } from '@/ui/PublicNavSignup';
|
||||
|
||||
export function AppHeader() {
|
||||
const pathname = usePathname();
|
||||
@@ -41,29 +44,39 @@ export function AppHeader() {
|
||||
return (
|
||||
<>
|
||||
<ShellHeader collapsed={isCollapsed}>
|
||||
{/* Left: Context & Search */}
|
||||
{/* Left: Public Navigation & Context */}
|
||||
<Box display="flex" alignItems="center" gap={6} flex={1}>
|
||||
<Text size="sm" variant="med" weight="medium" style={{ minWidth: '100px' }}>
|
||||
{breadcrumbs}
|
||||
</Text>
|
||||
{/* Public Top Navigation - Only when not authenticated */}
|
||||
{!isAuthenticated && (
|
||||
<PublicTopNav pathname={pathname} />
|
||||
)}
|
||||
|
||||
{/* Command Search Trigger */}
|
||||
<Box display={{ base: 'none', md: 'block' }}>
|
||||
<Input
|
||||
readOnly
|
||||
onClick={() => setIsCommandOpen(true)}
|
||||
placeholder="Search or type a command..."
|
||||
variant="search"
|
||||
width="24rem"
|
||||
rightElement={
|
||||
<Box display="flex" alignItems="center" gap={1} paddingX={1.5} paddingY={0.5} rounded bg="white/5" border>
|
||||
<Command size={10} />
|
||||
<Text size="xs" font="mono" variant="low" style={{ fontSize: '10px' }}>K</Text>
|
||||
</Box>
|
||||
}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</Box>
|
||||
{/* Context & Search - Only when authenticated */}
|
||||
{isAuthenticated && (
|
||||
<>
|
||||
<Text size="sm" variant="med" weight="medium" style={{ minWidth: '100px' }}>
|
||||
{breadcrumbs}
|
||||
</Text>
|
||||
|
||||
{/* Command Search Trigger */}
|
||||
<Box display={{ base: 'none', md: 'block' }}>
|
||||
<Input
|
||||
readOnly
|
||||
onClick={() => setIsCommandOpen(true)}
|
||||
placeholder="Search or type a command..."
|
||||
variant="search"
|
||||
width="24rem"
|
||||
rightElement={
|
||||
<Box display="flex" alignItems="center" gap={1} paddingX={1.5} paddingY={0.5} rounded bg="white/5" border>
|
||||
<Command size={10} />
|
||||
<Text size="xs" font="mono" variant="low" style={{ fontSize: '10px' }}>K</Text>
|
||||
</Box>
|
||||
}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Right: User & Notifications */}
|
||||
@@ -71,17 +84,25 @@ export function AppHeader() {
|
||||
{/* Notifications - Only when authed */}
|
||||
{isAuthenticated && (
|
||||
<Box position="relative">
|
||||
<IconButton
|
||||
icon={Bell}
|
||||
variant="ghost"
|
||||
<IconButton
|
||||
icon={Bell}
|
||||
variant="ghost"
|
||||
title="Notifications"
|
||||
/>
|
||||
<Box position="absolute" top={2} right={2} width={1.5} height={1.5} bg="var(--ui-color-intent-primary)" rounded="full" ring="2px" />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* User Pill (Handles Auth & Menu) */}
|
||||
<UserPill />
|
||||
{/* Public Login/Signup Buttons - Only when not authenticated */}
|
||||
{!isAuthenticated && (
|
||||
<>
|
||||
<PublicNavLogin />
|
||||
<PublicNavSignup />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* User Pill (Handles Auth & Menu) - Only when authenticated */}
|
||||
{isAuthenticated && <UserPill />}
|
||||
</Box>
|
||||
</ShellHeader>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ interface DeltaChipProps {
|
||||
export function DeltaChip({ value, type = 'rank' }: DeltaChipProps) {
|
||||
if (value === 0) {
|
||||
return (
|
||||
<Group gap={1}>
|
||||
<Group gap={1} data-testid="trend-indicator">
|
||||
<Icon icon={Minus} size={3} intent="low" />
|
||||
<Text size="xs" font="mono" variant="low">0</Text>
|
||||
</Group>
|
||||
@@ -26,7 +26,7 @@ export function DeltaChip({ value, type = 'rank' }: DeltaChipProps) {
|
||||
const absoluteValue = Math.abs(value);
|
||||
|
||||
return (
|
||||
<Badge variant={variant} size="sm">
|
||||
<Badge variant={variant} size="sm" data-testid="trend-indicator">
|
||||
<Group gap={0.5}>
|
||||
<Icon icon={IconComponent} size={3} />
|
||||
<Text size="xs" font="mono" weight="bold">
|
||||
|
||||
@@ -20,6 +20,7 @@ interface RankingRowProps {
|
||||
rating: number;
|
||||
wins: number;
|
||||
onClick?: () => void;
|
||||
droppedRaceIds?: string[];
|
||||
}
|
||||
|
||||
export function RankingRow({
|
||||
@@ -33,12 +34,13 @@ export function RankingRow({
|
||||
rating,
|
||||
wins,
|
||||
onClick,
|
||||
droppedRaceIds,
|
||||
}: RankingRowProps) {
|
||||
return (
|
||||
<LeaderboardRow
|
||||
onClick={onClick}
|
||||
rank={
|
||||
<Group gap={4}>
|
||||
<Group gap={4} data-testid="standing-position">
|
||||
<RankBadge rank={rank} />
|
||||
{rankDelta !== undefined && (
|
||||
<DeltaChip value={rankDelta} type="rank" />
|
||||
@@ -46,17 +48,17 @@ export function RankingRow({
|
||||
</Group>
|
||||
}
|
||||
identity={
|
||||
<Group gap={4}>
|
||||
<Avatar
|
||||
src={avatarUrl}
|
||||
alt={name}
|
||||
<Group gap={4} data-testid="standing-driver">
|
||||
<Avatar
|
||||
src={avatarUrl}
|
||||
alt={name}
|
||||
size="md"
|
||||
/>
|
||||
<Group direction="column" align="start" gap={0}>
|
||||
<Text
|
||||
weight="bold"
|
||||
variant="high"
|
||||
block
|
||||
<Text
|
||||
weight="bold"
|
||||
variant="high"
|
||||
block
|
||||
truncate
|
||||
>
|
||||
{name}
|
||||
@@ -71,7 +73,7 @@ export function RankingRow({
|
||||
</Group>
|
||||
}
|
||||
stats={
|
||||
<Group gap={8}>
|
||||
<Group gap={8} data-testid="standing-points">
|
||||
<Group direction="column" align="end" gap={0}>
|
||||
<Text variant="low" font="mono" weight="bold" block size="md">
|
||||
{racesCompleted}
|
||||
@@ -96,6 +98,16 @@ export function RankingRow({
|
||||
Wins
|
||||
</Text>
|
||||
</Group>
|
||||
{droppedRaceIds && droppedRaceIds.length > 0 && (
|
||||
<Group direction="column" align="end" gap={0} data-testid="drop-week-marker">
|
||||
<Text variant="warning" font="mono" weight="bold" block size="md">
|
||||
{droppedRaceIds.length}
|
||||
</Text>
|
||||
<Text size="xs" variant="low" block uppercase letterSpacing="widest" weight="bold">
|
||||
Dropped
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -28,7 +28,7 @@ export function AdminQuickViewWidgets({
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap={4}>
|
||||
<Stack gap={4} data-testid="admin-widgets">
|
||||
{/* Wallet Preview */}
|
||||
<Surface
|
||||
variant="precision"
|
||||
|
||||
@@ -129,7 +129,7 @@ export function EnhancedLeagueSchedulePanel({
|
||||
const isExpanded = expandedMonths.has(monthKey);
|
||||
|
||||
return (
|
||||
<Surface key={monthKey} variant="precision" overflow="hidden">
|
||||
<Surface key={monthKey} variant="precision" overflow="hidden" data-testid="schedule-month-group">
|
||||
{/* Month Header */}
|
||||
<Box
|
||||
display="flex"
|
||||
@@ -163,6 +163,7 @@ export function EnhancedLeagueSchedulePanel({
|
||||
key={race.id}
|
||||
variant="precision"
|
||||
p={4}
|
||||
data-testid="race-item"
|
||||
>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" gap={4}>
|
||||
{/* Race Info */}
|
||||
@@ -208,6 +209,7 @@ export function EnhancedLeagueSchedulePanel({
|
||||
size="sm"
|
||||
onClick={() => onRegister(race.id)}
|
||||
icon={<Icon icon={CheckCircle} size={3} />}
|
||||
data-testid="register-button"
|
||||
>
|
||||
Register
|
||||
</Button>
|
||||
|
||||
@@ -30,21 +30,22 @@ interface StandingEntry {
|
||||
|
||||
interface LeagueStandingsTableProps {
|
||||
standings: StandingEntry[];
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
export function LeagueStandingsTable({ standings }: LeagueStandingsTableProps) {
|
||||
export function LeagueStandingsTable({ standings, 'data-testid': dataTestId }: LeagueStandingsTableProps) {
|
||||
const router = useRouter();
|
||||
|
||||
if (!standings || standings.length === 0) {
|
||||
return (
|
||||
<Box p={12} textAlign="center" border borderColor="zinc-800" bg="zinc-900/30">
|
||||
<Box p={12} textAlign="center" border borderColor="zinc-800" bg="zinc-900/30" data-testid={dataTestId}>
|
||||
<Text color="text-zinc-500" italic>No standings data available for this season.</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LeaderboardTableShell>
|
||||
<LeaderboardTableShell data-testid={dataTestId}>
|
||||
<LeaderboardList>
|
||||
{standings.map((entry) => (
|
||||
<RankingRow
|
||||
@@ -60,6 +61,8 @@ export function LeagueStandingsTable({ standings }: LeagueStandingsTableProps) {
|
||||
rating={0}
|
||||
wins={entry.wins}
|
||||
onClick={entry.driverId ? () => router.push(routes.driver.detail(entry.driverId!)) : undefined}
|
||||
data-testid="standings-row"
|
||||
droppedRaceIds={entry.droppedRaceIds}
|
||||
/>
|
||||
))}
|
||||
</LeaderboardList>
|
||||
|
||||
@@ -74,6 +74,7 @@ export function NextRaceCountdownWidget({
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
data-testid="next-race-countdown"
|
||||
>
|
||||
<Stack
|
||||
position="absolute"
|
||||
|
||||
@@ -85,7 +85,7 @@ export function RaceDetailModal({
|
||||
mx={4}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Surface variant="precision" overflow="hidden">
|
||||
<Surface variant="precision" overflow="hidden" data-testid="race-detail-modal">
|
||||
{/* Header */}
|
||||
<Box
|
||||
display="flex"
|
||||
@@ -121,19 +121,19 @@ export function RaceDetailModal({
|
||||
Race Details
|
||||
</Text>
|
||||
<Stack gap={3}>
|
||||
<Group gap={2} align="center">
|
||||
<Group gap={2} align="center" data-testid="race-track">
|
||||
<Icon icon={MapPin} size={4} intent="primary" />
|
||||
<Text size="md" variant="high" weight="bold">
|
||||
{race.track || 'TBA'}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap={2} align="center">
|
||||
<Group gap={2} align="center" data-testid="race-car">
|
||||
<Icon icon={Car} size={4} intent="primary" />
|
||||
<Text size="md" variant="high">
|
||||
{race.car || 'TBA'}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap={2} align="center">
|
||||
<Group gap={2} align="center" data-testid="race-date">
|
||||
<Icon icon={Calendar} size={4} intent="primary" />
|
||||
<Text size="md" variant="high">
|
||||
{formatTime(race.scheduledAt)}
|
||||
|
||||
@@ -21,6 +21,7 @@ export function RosterTable({ members, isAdmin, onRemoveMember }: RosterTablePro
|
||||
members={members}
|
||||
isAdmin={isAdmin}
|
||||
onRemoveMember={onRemoveMember}
|
||||
data-testid="roster-table"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export function SeasonProgressWidget({
|
||||
variant="precision"
|
||||
rounded="xl"
|
||||
padding={6}
|
||||
data-testid="season-progress-bar"
|
||||
>
|
||||
<Stack gap={4}>
|
||||
{/* Header */}
|
||||
|
||||
@@ -20,11 +20,12 @@ interface TeamMembersTableProps {
|
||||
members: Member[];
|
||||
isAdmin?: boolean;
|
||||
onRemoveMember?: (driverId: string) => void;
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
export function TeamMembersTable({ members, isAdmin, onRemoveMember }: TeamMembersTableProps) {
|
||||
export function TeamMembersTable({ members, isAdmin, onRemoveMember, 'data-testid': dataTestId }: TeamMembersTableProps) {
|
||||
return (
|
||||
<Surface variant="precision" padding="none">
|
||||
<Surface variant="precision" padding="none" data-testid={dataTestId}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableHeaderCell>Personnel</TableHeaderCell>
|
||||
@@ -35,30 +36,30 @@ export function TeamMembersTable({ members, isAdmin, onRemoveMember }: TeamMembe
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{members.map((member) => (
|
||||
<TableRow key={member.driverId}>
|
||||
<TableRow key={member.driverId} data-testid="driver-card">
|
||||
<TableCell>
|
||||
<Stack direction="row" align="center" gap="sm">
|
||||
<Box
|
||||
width={10}
|
||||
height={10}
|
||||
bg="var(--ui-color-bg-base)"
|
||||
border="1px solid var(--ui-color-border-muted)"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
<Box
|
||||
width={10}
|
||||
height={10}
|
||||
bg="var(--ui-color-bg-base)"
|
||||
border="1px solid var(--ui-color-border-muted)"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
rounded="md"
|
||||
>
|
||||
<Text size="xs" weight="bold" variant="primary" mono>{member.driverName.substring(0, 2).toUpperCase()}</Text>
|
||||
</Box>
|
||||
<Text weight="bold" size="sm">{member.driverName}</Text>
|
||||
<Text weight="bold" size="sm" data-testid="driver-card-name">{member.driverName}</Text>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box
|
||||
paddingX={2}
|
||||
paddingY={0.5}
|
||||
bg="rgba(255,255,255,0.02)"
|
||||
border="1px solid var(--ui-color-border-muted)"
|
||||
<Box
|
||||
paddingX={2}
|
||||
paddingY={0.5}
|
||||
bg="rgba(255,255,255,0.02)"
|
||||
border="1px solid var(--ui-color-border-muted)"
|
||||
display="inline-block"
|
||||
rounded="sm"
|
||||
>
|
||||
@@ -71,15 +72,16 @@ export function TeamMembersTable({ members, isAdmin, onRemoveMember }: TeamMembe
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell textAlign="right">
|
||||
<Text mono weight="bold" variant="primary">1450</Text>
|
||||
<Text mono weight="bold" variant="primary" data-testid="driver-card-stats">1450</Text>
|
||||
</TableCell>
|
||||
{isAdmin && (
|
||||
<TableCell textAlign="right">
|
||||
{member.role !== 'owner' && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => onRemoveMember?.(member.driverId)}
|
||||
data-testid="admin-actions"
|
||||
>
|
||||
DECOMMISSION
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user