website refactor
This commit is contained in:
@@ -1,52 +0,0 @@
|
||||
import { Image } from '@/ui/Image';
|
||||
import { Stack } from '@/ui/primitives/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
|
||||
|
||||
export function ActiveDriverCard({
|
||||
name,
|
||||
avatarUrl,
|
||||
categoryLabel,
|
||||
categoryColor,
|
||||
skillLevelLabel,
|
||||
skillLevelColor,
|
||||
onClick,
|
||||
}: ActiveDriverCardProps) {
|
||||
return (
|
||||
<Stack
|
||||
as="button"
|
||||
{...({ type: 'button' } as any)}
|
||||
onClick={onClick}
|
||||
p={3}
|
||||
rounded="xl"
|
||||
bg="bg-iron-gray/40"
|
||||
border
|
||||
borderColor="border-charcoal-outline"
|
||||
className="transition-all hover:border-performance-green/40 group text-center"
|
||||
>
|
||||
<Stack position="relative" w="12" h="12" mx="auto" rounded="full" overflow="hidden" border borderColor="border-charcoal-outline" mb={2}>
|
||||
<Image src={avatarUrl || '/default-avatar.png'} alt={name} objectFit="cover" fill />
|
||||
<Stack position="absolute" bottom="0" right="0" w="3" h="3" rounded="full" bg="bg-performance-green" border borderColor="border-iron-gray" style={{ borderWidth: '2px' }}>{null}</Stack>
|
||||
</Stack>
|
||||
<Text
|
||||
size="sm"
|
||||
weight="medium"
|
||||
color="text-white"
|
||||
truncate
|
||||
block
|
||||
groupHoverTextColor="performance-green"
|
||||
transition
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
<Stack direction="row" align="center" justify="center" gap={1}>
|
||||
{categoryLabel && (
|
||||
<Text size="xs" color={categoryColor}>{categoryLabel}</Text>
|
||||
)}
|
||||
{skillLevelLabel && (
|
||||
<Text size="xs" color={skillLevelColor}>{skillLevelLabel}</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { Badge } from '@/ui/Badge';
|
||||
import { Image } from '@/ui/Image';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { PlaceholderImage } from '@/ui/PlaceholderImage';
|
||||
import { Box } from '@/ui/primitives/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
|
||||
export interface DriverIdentityProps {
|
||||
driver: {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl: string | null;
|
||||
};
|
||||
href?: string;
|
||||
contextLabel?: React.ReactNode;
|
||||
meta?: React.ReactNode;
|
||||
size?: 'sm' | 'md';
|
||||
}
|
||||
|
||||
export function DriverIdentity(props: DriverIdentityProps) {
|
||||
const { driver, href, contextLabel, meta, size = 'md' } = props;
|
||||
|
||||
const avatarSize = size === 'sm' ? 40 : 48;
|
||||
const nameSize = size === 'sm' ? 'sm' : 'base';
|
||||
|
||||
const avatarUrl = driver.avatarUrl;
|
||||
|
||||
const content = (
|
||||
<Box display="flex" alignItems="center" gap={{ base: 3, md: 4 }} flexGrow={1} minWidth="0">
|
||||
<Box
|
||||
rounded="full"
|
||||
bg="bg-primary-blue/20"
|
||||
overflow="hidden"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
flexShrink={0}
|
||||
w={`${avatarSize}px`}
|
||||
h={`${avatarSize}px`}
|
||||
>
|
||||
{avatarUrl ? (
|
||||
<Image
|
||||
src={avatarUrl}
|
||||
alt={driver.name}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
fullWidth
|
||||
fullHeight
|
||||
objectFit="cover"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage size={avatarSize} />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flexGrow={1} minWidth="0">
|
||||
<Box display="flex" alignItems="center" gap={2} minWidth="0">
|
||||
<Text size={nameSize} weight="medium" color="text-white" truncate>
|
||||
{driver.name}
|
||||
</Text>
|
||||
{contextLabel && (
|
||||
<Badge variant="default" bg="bg-charcoal-outline/60" size="xs">
|
||||
{contextLabel}
|
||||
</Badge>
|
||||
)}
|
||||
</Box>
|
||||
{meta && (
|
||||
<Text size="xs" color="text-gray-400" mt={0.5} truncate block>
|
||||
{meta}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<Link href={href} block variant="ghost">
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return <Box display="flex" alignItems="center" gap={{ base: 3, md: 4 }} flexGrow={1} minWidth="0">{content}</Box>;
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Panel } from '@/ui/Panel';
|
||||
import { RankingListItem } from '@/components/leaderboards/RankingListItem';
|
||||
import { RankingList } from '@/components/leaderboards/RankingList';
|
||||
import { MinimalEmptyState } from '@/components/shared/state/EmptyState';
|
||||
import { EmptyState } from '@/ui/EmptyState';
|
||||
|
||||
export interface DriverRanking {
|
||||
type: 'overall' | 'league';
|
||||
@@ -21,19 +22,18 @@ interface DriverRankingsProps {
|
||||
export function DriverRankings({ rankings }: DriverRankingsProps) {
|
||||
if (!rankings || rankings.length === 0) {
|
||||
return (
|
||||
<Card bg="bg-iron-gray/60" borderColor="border-charcoal-outline/80" p={4}>
|
||||
<Heading level={3} mb={2}>Rankings</Heading>
|
||||
<MinimalEmptyState
|
||||
<Panel title="Rankings">
|
||||
<EmptyState
|
||||
title="No ranking data available yet"
|
||||
description="Compete in leagues to earn your first results."
|
||||
variant="minimal"
|
||||
/>
|
||||
</Card>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card bg="bg-iron-gray/60" borderColor="border-charcoal-outline/80" p={4}>
|
||||
<Heading level={3} mb={4}>Rankings</Heading>
|
||||
<Panel title="Rankings">
|
||||
<RankingList>
|
||||
{rankings.map((ranking, index) => (
|
||||
<RankingListItem
|
||||
@@ -47,6 +47,6 @@ export function DriverRankings({ rankings }: DriverRankingsProps) {
|
||||
/>
|
||||
))}
|
||||
</RankingList>
|
||||
</Card>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { EmptyState } from '@/components/shared/state/EmptyState';
|
||||
import { LoadingWrapper } from '@/components/shared/state/LoadingWrapper';
|
||||
import { EmptyState } from '@/ui/EmptyState';
|
||||
import { LoadingWrapper } from '@/ui/LoadingWrapper';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Pagination } from '@/ui/Pagination';
|
||||
import { Stack } from '@/ui/primitives/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { ControlBar } from '@/ui/ControlBar';
|
||||
import { Trophy } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
interface RaceHistoryProps {
|
||||
driverId: string;
|
||||
@@ -24,7 +25,6 @@ export function ProfileRaceHistory({ driverId }: RaceHistoryProps) {
|
||||
async function loadRaceHistory() {
|
||||
try {
|
||||
// Driver race history is not exposed via API yet.
|
||||
// Keep as placeholder until an endpoint exists.
|
||||
} catch (err) {
|
||||
console.error('Failed to load race history:', err);
|
||||
} finally {
|
||||
@@ -40,18 +40,7 @@ export function ProfileRaceHistory({ driverId }: RaceHistoryProps) {
|
||||
const totalPages = Math.ceil(filteredResults.length / resultsPerPage);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Stack gap={4}>
|
||||
<Stack display="flex" alignItems="center" gap={2}>
|
||||
{[1, 2, 3].map(i => (
|
||||
<Stack key={i} h="9" w="24" bg="bg-iron-gray" rounded="md" animate="pulse" />
|
||||
))}
|
||||
</Stack>
|
||||
<Card>
|
||||
<LoadingWrapper variant="skeleton" skeletonCount={3} />
|
||||
</Card>
|
||||
</Stack>
|
||||
);
|
||||
return <LoadingWrapper variant="spinner" message="Loading race history..." />;
|
||||
}
|
||||
|
||||
if (filteredResults.length === 0) {
|
||||
@@ -60,50 +49,50 @@ export function ProfileRaceHistory({ driverId }: RaceHistoryProps) {
|
||||
icon={Trophy}
|
||||
title="No race history yet"
|
||||
description="Complete races to build your racing record"
|
||||
variant="minimal"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap={4}>
|
||||
<Stack display="flex" alignItems="center" gap={2}>
|
||||
<Button
|
||||
variant={filter === 'all' ? 'primary' : 'secondary'}
|
||||
onClick={() => { setFilter('all'); setPage(1); }}
|
||||
size="sm"
|
||||
>
|
||||
All Races
|
||||
</Button>
|
||||
<Button
|
||||
variant={filter === 'wins' ? 'primary' : 'secondary'}
|
||||
onClick={() => { setFilter('wins'); setPage(1); }}
|
||||
size="sm"
|
||||
>
|
||||
Wins Only
|
||||
</Button>
|
||||
<Button
|
||||
variant={filter === 'podiums' ? 'primary' : 'secondary'}
|
||||
onClick={() => { setFilter('podiums'); setPage(1); }}
|
||||
size="sm"
|
||||
>
|
||||
Podiums
|
||||
</Button>
|
||||
</Stack>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<ControlBar>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<Button
|
||||
variant={filter === 'all' ? 'primary' : 'secondary'}
|
||||
onClick={() => { setFilter('all'); setPage(1); }}
|
||||
size="sm"
|
||||
>
|
||||
All Races
|
||||
</Button>
|
||||
<Button
|
||||
variant={filter === 'wins' ? 'primary' : 'secondary'}
|
||||
onClick={() => { setFilter('wins'); setPage(1); }}
|
||||
size="sm"
|
||||
>
|
||||
Wins Only
|
||||
</Button>
|
||||
<Button
|
||||
variant={filter === 'podiums' ? 'primary' : 'secondary'}
|
||||
onClick={() => { setFilter('podiums'); setPage(1); }}
|
||||
size="sm"
|
||||
>
|
||||
Podiums
|
||||
</Button>
|
||||
</div>
|
||||
</ControlBar>
|
||||
|
||||
<Card>
|
||||
{/* No results until API provides driver results */}
|
||||
<Stack minHeight="100px" display="flex" center>
|
||||
<Text color="text-gray-500">No results found for the selected filter.</Text>
|
||||
</Stack>
|
||||
<div style={{ minHeight: '10rem', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Text variant="low">No results found for the selected filter.</Text>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
currentPage={page}
|
||||
totalPages={totalPages}
|
||||
totalItems={filteredResults.length}
|
||||
itemsPerPage={resultsPerPage}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
</Card>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user