website refactor

This commit is contained in:
2026-01-19 14:07:49 +01:00
parent 54f42bab9f
commit 6154d54435
88 changed files with 755 additions and 566 deletions

View File

@@ -17,7 +17,7 @@ interface Achievement {
description: string;
icon: string;
rarity: 'common' | 'rare' | 'epic' | 'legendary' | string;
earnedAt: Date;
earnedAtLabel: string;
}
interface AchievementGridProps {
@@ -72,7 +72,7 @@ export function AchievementGrid({ achievements }: AchievementGridProps) {
</Text>
<Text size="xs" variant="low"></Text>
<Text size="xs" variant="low">
{AchievementDisplay.formatDate(achievement.earnedAt)}
{achievement.earnedAtLabel}
</Text>
</Group>
</Stack>

View File

@@ -9,7 +9,8 @@ interface DriverHeaderPanelProps {
avatarUrl?: string;
nationality: string;
rating: number;
globalRank?: number | null;
ratingLabel: string;
globalRankLabel?: string | null;
bio?: string | null;
actions?: React.ReactNode;
}
@@ -19,7 +20,8 @@ export function DriverHeaderPanel({
avatarUrl,
nationality,
rating,
globalRank,
ratingLabel,
globalRankLabel,
bio,
actions
}: DriverHeaderPanelProps) {
@@ -54,8 +56,8 @@ export function DriverHeaderPanel({
rounded="2xl"
overflow="hidden"
border
borderColor="border-charcoal-outline"
bg="bg-graphite-black"
borderColor="border-charcoal-outline"
bg="bg-graphite-black"
flexShrink={0}
>
<Image
@@ -73,16 +75,16 @@ export function DriverHeaderPanel({
<Text as="h1" size="3xl" weight="bold" color="text-white">
{name}
</Text>
<RatingBadge rating={rating} size="lg" />
<RatingBadge rating={rating} ratingLabel={ratingLabel} size="lg" />
</Stack>
<Stack direction="row" align="center" gap={4} wrap>
<Text size="sm" color="text-gray-400">
{nationality}
</Text>
{globalRank !== undefined && globalRank !== null && (
{globalRankLabel && (
<Text size="sm" color="text-gray-400">
Global Rank: <Text color="text-warning-amber" weight="semibold">#{globalRank}</Text>
Global Rank: <Text color="text-warning-amber" weight="semibold">{globalRankLabel}</Text>
</Text>
)}
</Stack>

View File

@@ -14,8 +14,10 @@ interface DriverProfileHeaderProps {
avatarUrl?: string | null;
nationality: string;
rating: number;
ratingLabel: string;
safetyRating?: number;
globalRank?: number;
safetyRatingLabel: string;
globalRankLabel?: string;
bio?: string | null;
friendRequestSent: boolean;
onAddFriend: () => void;
@@ -26,8 +28,10 @@ export function DriverProfileHeader({
avatarUrl,
nationality,
rating,
ratingLabel,
safetyRating = 92,
globalRank,
safetyRatingLabel,
globalRankLabel,
bio,
friendRequestSent,
onAddFriend,
@@ -56,11 +60,11 @@ export function DriverProfileHeader({
<Stack>
<Stack direction="row" align="center" gap={3} mb={1}>
<Heading level={1}>{name}</Heading>
{globalRank && (
{globalRankLabel && (
<Stack display="flex" alignItems="center" gap={1} rounded="md" bg="bg-warning-amber/10" px={2} py={0.5} border borderColor="border-warning-amber/20">
<Trophy size={12} color="#FFBE4D" />
<Text size="xs" weight="bold" font="mono" color="text-warning-amber">
#{globalRank}
{globalRankLabel}
</Text>
</Stack>
)}
@@ -72,8 +76,8 @@ export function DriverProfileHeader({
</Stack>
<Stack w="1" h="1" rounded="full" bg="bg-gray-700" />
<Stack direction="row" align="center" gap={2}>
<RatingBadge rating={rating} size="sm" />
<SafetyRatingBadge rating={safetyRating} size="sm" />
<RatingBadge rating={rating} ratingLabel={ratingLabel} size="sm" />
<SafetyRatingBadge rating={safetyRating} ratingLabel={safetyRatingLabel} size="sm" />
</Stack>
</Stack>
</Stack>

View File

@@ -13,7 +13,8 @@ interface DriverTableRowProps {
avatarUrl?: string | null;
nationality: string;
rating: number;
wins: number;
ratingLabel: string;
winsLabel: string;
onClick: () => void;
}
@@ -23,7 +24,8 @@ export function DriverTableRow({
avatarUrl,
nationality,
rating,
wins,
ratingLabel,
winsLabel,
onClick,
}: DriverTableRowProps) {
return (
@@ -58,11 +60,11 @@ export function DriverTableRow({
<Text size="xs" variant="low">{nationality}</Text>
</TableCell>
<TableCell textAlign="right">
<RatingBadge rating={rating} size="sm" />
<RatingBadge rating={rating} ratingLabel={ratingLabel} size="sm" />
</TableCell>
<TableCell textAlign="right">
<Text size="sm" weight="semibold" font="mono" variant="success">
{wins}
{winsLabel}
</Text>
</TableCell>
</TableRow>

View File

@@ -17,25 +17,25 @@ interface DriverStat {
}
interface DriversDirectoryHeaderProps {
totalDrivers: number;
activeDrivers: number;
totalWins: number;
totalRaces: number;
totalDriversLabel: string;
activeDriversLabel: string;
totalWinsLabel: string;
totalRacesLabel: string;
onViewLeaderboard: () => void;
}
export function DriversDirectoryHeader({
totalDrivers,
activeDrivers,
totalWins,
totalRaces,
totalDriversLabel,
activeDriversLabel,
totalWinsLabel,
totalRacesLabel,
onViewLeaderboard,
}: DriversDirectoryHeaderProps) {
const stats: DriverStat[] = [
{ label: 'drivers', value: totalDrivers, intent: 'primary' },
{ label: 'active', value: activeDrivers, intent: 'success' },
{ label: 'total wins', value: totalWins.toLocaleString(), intent: 'warning' },
{ label: 'races', value: totalRaces.toLocaleString(), intent: 'telemetry' },
{ label: 'drivers', value: totalDriversLabel, intent: 'primary' },
{ label: 'active', value: activeDriversLabel, intent: 'success' },
{ label: 'total wins', value: totalWinsLabel, intent: 'warning' },
{ label: 'races', value: totalRacesLabel, intent: 'telemetry' },
];
return (

View File

@@ -33,9 +33,9 @@ interface FeaturedDriverCardProps {
name: string;
nationality: string;
avatarUrl?: string;
rating: number;
wins: number;
podiums: number;
ratingLabel: string;
winsLabel: string;
podiumsLabel: string;
skillLevel?: string;
category?: string;
};
@@ -142,17 +142,17 @@ export function FeaturedDriverCard({ driver, position, onClick }: FeaturedDriver
<Box display="grid" gridCols={3} gap={3}>
<MiniStat
label="Rating"
value={driver.rating.toLocaleString()}
value={driver.ratingLabel}
color="text-primary-blue"
/>
<MiniStat
label="Wins"
value={driver.wins}
value={driver.winsLabel}
color="text-performance-green"
/>
<MiniStat
label="Podiums"
value={driver.podiums}
value={driver.podiumsLabel}
color="text-warning-amber"
/>
</Box>

View File

@@ -17,12 +17,12 @@ interface ProfileHeroProps {
avatarUrl?: string;
country: string;
iracingId: number;
joinedAt: string | Date;
joinedAtLabel: string;
};
stats: {
rating: number;
ratingLabel: string;
} | null;
globalRank: number;
globalRankLabel: string;
timezone: string;
socialHandles: {
platform: string;
@@ -47,7 +47,7 @@ function getSocialIcon(platform: string) {
export function ProfileHero({
driver,
stats,
globalRank,
globalRankLabel,
timezone,
socialHandles,
onAddFriend,
@@ -87,14 +87,14 @@ export function ProfileHero({
<Surface variant="muted" rounded="lg" padding={1} style={{ backgroundColor: 'rgba(59, 130, 246, 0.1)', border: '1px solid rgba(59, 130, 246, 0.3)', paddingLeft: '0.75rem', paddingRight: '0.75rem' }}>
<Stack direction="row" align="center" gap={2}>
<Star style={{ width: '1rem', height: '1rem', color: '#3b82f6' }} />
<Text font="mono" weight="bold" color="text-primary-blue">{stats.rating}</Text>
<Text font="mono" weight="bold" color="text-primary-blue">{stats.ratingLabel}</Text>
<Text size="xs" color="text-gray-400">Rating</Text>
</Stack>
</Surface>
<Surface variant="muted" rounded="lg" padding={1} style={{ backgroundColor: 'rgba(250, 204, 21, 0.1)', border: '1px solid rgba(250, 204, 21, 0.3)', paddingLeft: '0.75rem', paddingRight: '0.75rem' }}>
<Stack direction="row" align="center" gap={2}>
<Trophy style={{ width: '1rem', height: '1rem', color: '#facc15' }} />
<Text font="mono" weight="bold" style={{ color: '#facc15' }}>#{globalRank}</Text>
<Text font="mono" weight="bold" style={{ color: '#facc15' }}>{globalRankLabel}</Text>
<Text size="xs" color="text-gray-400">Global</Text>
</Stack>
</Surface>
@@ -111,11 +111,7 @@ export function ProfileHero({
<Stack direction="row" align="center" gap={1.5}>
<Calendar style={{ width: '1rem', height: '1rem' }} />
<Text size="sm">
Joined{' '}
{new Date(driver.joinedAt).toLocaleDateString('en-US', {
month: 'short',
year: 'numeric',
})}
Joined {driver.joinedAtLabel}
</Text>
</Stack>
<Stack direction="row" align="center" gap={1.5}>

View File

@@ -3,10 +3,11 @@ import { Badge } from '@/ui/Badge';
interface RatingBadgeProps {
rating: number;
ratingLabel: string;
size?: 'sm' | 'md' | 'lg';
}
export function RatingBadge({ rating, size = 'md' }: RatingBadgeProps) {
export function RatingBadge({ rating, ratingLabel, size = 'md' }: RatingBadgeProps) {
const badgeSize = size === 'lg' ? 'md' : size;
const getVariant = (val: number): 'warning' | 'primary' | 'success' | 'default' => {
@@ -22,7 +23,7 @@ export function RatingBadge({ rating, size = 'md' }: RatingBadgeProps) {
variant={getVariant(rating)}
size={badgeSize}
>
{rating.toLocaleString()}
{ratingLabel}
</Badge>
);
}

View File

@@ -6,10 +6,11 @@ import { Shield } from 'lucide-react';
interface SafetyRatingBadgeProps {
rating: number;
ratingLabel: string;
size?: 'sm' | 'md' | 'lg';
}
export function SafetyRatingBadge({ rating, size = 'md' }: SafetyRatingBadgeProps) {
export function SafetyRatingBadge({ rating, ratingLabel, size = 'md' }: SafetyRatingBadgeProps) {
const getColor = (r: number) => {
if (r >= 90) return 'text-performance-green';
if (r >= 70) return 'text-warning-amber';
@@ -65,7 +66,7 @@ export function SafetyRatingBadge({ rating, size = 'md' }: SafetyRatingBadgeProp
font="mono"
color={colorClass}
>
SR {rating.toFixed(0)}
{ratingLabel}
</Text>
</Box>
);

View File

@@ -29,6 +29,12 @@ import {
} from 'lucide-react';
import { useEffect, useState } from 'react';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import { DurationDisplay } from '@/lib/display-objects/DurationDisplay';
import { MemoryDisplay } from '@/lib/display-objects/MemoryDisplay';
import { PercentDisplay } from '@/lib/display-objects/PercentDisplay';
import { TimeDisplay } from '@/lib/display-objects/TimeDisplay';
interface ErrorAnalyticsDashboardProps {
/**
* Auto-refresh interval in milliseconds
@@ -41,16 +47,16 @@ interface ErrorAnalyticsDashboardProps {
}
function formatDuration(duration: number): string {
return duration.toFixed(2) + 'ms';
return DurationDisplay.formatMs(duration);
}
function formatPercentage(value: number, total: number): string {
if (total === 0) return '0%';
return ((value / total) * 100).toFixed(1) + '%';
return PercentDisplay.format(value / total);
}
function formatMemory(bytes: number): string {
return (bytes / 1024 / 1024).toFixed(1) + 'MB';
return MemoryDisplay.formatMB(bytes);
}
interface PerformanceWithMemory extends Performance {
@@ -321,7 +327,7 @@ export function ErrorAnalyticsDashboard({
<Stack display="flex" justifyContent="between" alignItems="start" gap={2} mb={1}>
<Text size="xs" font="mono" weight="bold" color="text-red-400" truncate>{error.type}</Text>
<Text size="xs" color="text-gray-500" fontSize="10px">
{new Date(error.timestamp).toLocaleTimeString()}
{DateDisplay.formatTime(error.timestamp)}
</Text>
</Stack>
<Text size="xs" color="text-gray-300" block mb={1}>{error.message}</Text>

View File

@@ -8,6 +8,7 @@ import { Grid } from '@/ui/Grid';
import { Stack } from '@/ui/Stack';
import { Section } from '@/ui/Section';
import { Text } from '@/ui/Text';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
interface FeedItemData {
id: string;
@@ -46,6 +47,16 @@ export function FeedLayout({
upcomingRaces,
latestResults
}: FeedLayoutProps) {
const formattedUpcomingRaces = upcomingRaces.map(r => ({
...r,
formattedDate: DateDisplay.formatShort(r.scheduledAt),
}));
const formattedLatestResults = latestResults.map(r => ({
...r,
formattedDate: DateDisplay.formatShort(r.scheduledAt),
}));
return (
<Section className="mt-16 mb-20">
<Container>
@@ -64,8 +75,8 @@ export function FeedLayout({
</Card>
</Stack>
<Stack as="aside" gap={6}>
<UpcomingRacesSidebar races={upcomingRaces} />
<LatestResultsSidebar results={latestResults} />
<UpcomingRacesSidebar races={formattedUpcomingRaces} />
<LatestResultsSidebar results={formattedLatestResults} />
</Stack>
</Grid>
</Container>

View File

@@ -4,7 +4,7 @@ import { Text } from '@/ui/Text';
interface JoinRequestItemProps {
driverId: string;
requestedAt: string | Date;
formattedRequestedAt: string;
onApprove: () => void;
onReject: () => void;
isApproving?: boolean;
@@ -13,7 +13,7 @@ interface JoinRequestItemProps {
export function JoinRequestItem({
driverId,
requestedAt,
formattedRequestedAt,
onApprove,
onReject,
isApproving,
@@ -47,7 +47,7 @@ export function JoinRequestItem({
<Stack flexGrow={1}>
<Text color="text-white" weight="medium" block>{driverId}</Text>
<Text size="sm" color="text-gray-400" block>
Requested {new Date(requestedAt).toLocaleDateString()}
Requested {formattedRequestedAt}
</Text>
</Stack>
</Stack>

View File

@@ -1,6 +1,7 @@
import { ActivityFeedItem } from '@/components/feed/ActivityFeedItem';
import { useLeagueRaces } from "@/hooks/league/useLeagueRaces";
import { LeagueActivityService } from '@/lib/services/league/LeagueActivityService';
import { RelativeTimeDisplay } from '@/lib/display-objects/RelativeTimeDisplay';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
@@ -20,19 +21,6 @@ interface LeagueActivityFeedProps {
limit?: number;
}
function timeAgo(timestamp: Date): string {
const diffMs = Date.now() - timestamp.getTime();
const diffMinutes = Math.floor(diffMs / 60000);
if (diffMinutes < 1) return 'Just now';
if (diffMinutes < 60) return `${diffMinutes} min ago`;
const diffHours = Math.floor(diffMinutes / 60);
if (diffHours < 24) return `${diffHours}h ago`;
const diffDays = Math.floor(diffHours / 24);
if (diffDays === 1) return 'Yesterday';
if (diffDays < 7) return `${diffDays}d ago`;
return timestamp.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}
export function LeagueActivityFeed({ leagueId, limit = 10 }: LeagueActivityFeedProps) {
const { data: raceList = [], isLoading } = useLeagueRaces(leagueId);
@@ -140,7 +128,7 @@ function ActivityItem({ activity }: { activity: LeagueActivity }) {
<ActivityFeedItem
icon={getIcon()}
content={getContent()}
timestamp={timeAgo(activity.timestamp)}
timestamp={RelativeTimeDisplay.format(activity.timestamp, new Date())}
/>
);
}

View File

@@ -27,6 +27,9 @@ import { Icon } from '@/ui/Icon';
import { Card } from '@/ui/Card';
import { Grid } from '@/ui/Grid';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import { DurationDisplay } from '@/lib/display-objects/DurationDisplay';
interface LeagueReviewSummaryProps {
form: LeagueConfigFormModel;
presets: LeagueScoringPresetViewModel[];
@@ -139,11 +142,7 @@ export function LeagueReviewSummary({ form, presets }: LeagueReviewSummaryProps)
const seasonStartLabel =
timings.seasonStartDate
? new Date(timings.seasonStartDate).toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
})
? DateDisplay.formatShort(timings.seasonStartDate)
: null;
const stewardingLabel = (() => {

View File

@@ -194,17 +194,10 @@ export function LeagueSchedule({ leagueId, onRaceClick }: LeagueScheduleProps) {
<Stack display="flex" alignItems="center" gap={3}>
<Stack textAlign="right">
<Text color="text-white" weight="medium" block>
{race.scheduledAt.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
{race.formattedDate}
</Text>
<Text size="sm" color="text-gray-400" block>
{race.scheduledAt.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
{race.formattedTime}
</Text>
{isPast && race.status === 'completed' && (
<Text size="xs" color="text-primary-blue" mt={1} block>View Results </Text>

View File

@@ -286,7 +286,7 @@ export function ReviewProtestModal({
<Stack direction="row" align="center" justify="between">
<Text size="sm" color="text-gray-400">Filed Date</Text>
<Text size="sm" color="text-white" weight="medium">
{new Date(protest.filedAt || protest.submittedAt).toLocaleString()}
{protest.formattedSubmittedAt}
</Text>
</Stack>
<Stack direction="row" align="center" justify="between">
@@ -299,7 +299,7 @@ export function ReviewProtestModal({
<Text size="sm" color="text-gray-400">Status</Text>
<Stack as="span" px={2} py={1} rounded="sm" bg="bg-orange-500/20">
<Text size="xs" weight="medium" color="text-orange-400">
{protest.status}
{protest.statusDisplay}
</Text>
</Stack>
</Stack>

View File

@@ -15,8 +15,10 @@ interface Race {
name: string;
track?: string;
car?: string;
scheduledAt: string;
formattedDate: string;
formattedTime: string;
status: string;
statusLabel: string;
sessionType?: string;
isPast?: boolean;
}
@@ -33,19 +35,19 @@ export function ScheduleRaceCard({ race }: ScheduleRaceCardProps) {
<Stack w="3" h="3" rounded="full" bg={race.isPast ? 'bg-performance-green' : 'bg-primary-blue'} />
<Heading level={3} fontSize="lg">{race.name}</Heading>
<Badge variant={race.status === 'completed' ? 'success' : 'primary'}>
{race.status === 'completed' ? 'Completed' : 'Scheduled'}
{race.statusLabel}
</Badge>
</Stack>
<Grid cols={4} gap={4}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Calendar} size={4} color="#9ca3af" />
<Text size="sm" color="text-gray-300">{new Date(race.scheduledAt).toLocaleDateString()}</Text>
<Text size="sm" color="text-gray-300">{race.formattedDate}</Text>
</Stack>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Clock} size={4} color="#9ca3af" />
<Text size="sm" color="text-gray-300">{new Date(race.scheduledAt).toLocaleTimeString()}</Text>
<Text size="sm" color="text-gray-300">{race.formattedTime}</Text>
</Stack>
{race.track && (

View File

@@ -11,7 +11,8 @@ interface SponsorshipRequest {
id: string;
sponsorName: string;
status: 'pending' | 'approved' | 'rejected';
requestedAt: string;
statusLabel: string;
formattedRequestedAt: string;
slotName: string;
}
@@ -57,7 +58,7 @@ export function SponsorshipRequestCard({ request }: SponsorshipRequestCardProps)
<Icon icon={statusIcon} size={5} color={statusColor} />
<Text weight="semibold" color="text-white">{request.sponsorName}</Text>
<Badge variant={statusVariant}>
{request.status}
{request.statusLabel}
</Badge>
</Stack>
@@ -66,7 +67,7 @@ export function SponsorshipRequestCard({ request }: SponsorshipRequestCardProps)
</Text>
<Text size="xs" color="text-gray-400" block>
{new Date(request.requestedAt).toLocaleDateString()}
{request.formattedRequestedAt}
</Text>
</Stack>
</Stack>

View File

@@ -55,12 +55,12 @@ interface StandingsTableProps {
standings: Array<{
driverId: string;
position: number;
totalPoints: number;
racesFinished: number;
racesStarted: number;
avgFinish: number | null;
penaltyPoints: number;
bonusPoints: number;
positionLabel: string;
totalPointsLabel: string;
racesLabel: string;
avgFinishLabel: string;
penaltyPointsLabel: string;
bonusPointsLabel: string;
teamName?: string;
}>;
drivers: Array<{
@@ -508,7 +508,7 @@ export function StandingsTable({
'text-white'
}
>
{row.position}
{row.positionLabel}
</Stack>
</TableCell>
@@ -625,7 +625,7 @@ export function StandingsTable({
{/* Total Points with Hover Action */}
<TableCell textAlign="right" position="relative">
<Stack display="flex" alignItems="center" justifyContent="end" gap={2}>
<Text color="text-white" weight="bold" size="lg">{row.totalPoints}</Text>
<Text color="text-white" weight="bold" size="lg">{row.totalPointsLabel}</Text>
{isAdmin && canModify && (
<Stack
as="button"
@@ -650,28 +650,27 @@ export function StandingsTable({
{/* Races (Finished/Started) */}
<TableCell textAlign="center">
<Text color="text-white">{row.racesFinished}</Text>
<Text color="text-gray-500">/{row.racesStarted}</Text>
<Text color="text-white">{row.racesLabel}</Text>
</TableCell>
{/* Avg Finish */}
<TableCell textAlign="right">
<Text color="text-gray-300">
{row.avgFinish !== null ? row.avgFinish.toFixed(1) : '—'}
{row.avgFinishLabel}
</Text>
</TableCell>
{/* Penalty */}
<TableCell textAlign="right">
<Text color={row.penaltyPoints > 0 ? 'text-red-400' : 'text-gray-500'} weight={row.penaltyPoints > 0 ? 'medium' : 'normal'}>
{row.penaltyPoints > 0 ? `-${row.penaltyPoints}` : '—'}
<Text color="text-gray-500">
{row.penaltyPointsLabel}
</Text>
</TableCell>
{/* Bonus */}
<TableCell textAlign="right">
<Text color={row.bonusPoints !== 0 ? 'text-green-400' : 'text-gray-500'} weight={row.bonusPoints !== 0 ? 'medium' : 'normal'}>
{row.bonusPoints !== 0 ? `+${row.bonusPoints}` : '—'}
<Text color="text-gray-500">
{row.bonusPointsLabel}
</Text>
</TableCell>
</TableRow>

View File

@@ -13,7 +13,7 @@ interface Protest {
protestingDriver: string;
accusedDriver: string;
description: string;
submittedAt: string;
formattedSubmittedAt: string;
status: 'pending' | 'under_review' | 'resolved' | 'rejected';
}
@@ -63,7 +63,7 @@ export function StewardingQueuePanel({ protests, onReview }: StewardingQueuePane
<Stack direction="row" align="center" gap={1.5}>
<Icon icon={Clock} size={3} color="text-gray-600" />
<Text size="xs" color="text-gray-500">
{new Date(protest.submittedAt).toLocaleString()}
{protest.formattedSubmittedAt}
</Text>
</Stack>
</Stack>

View File

@@ -9,21 +9,21 @@ import { ArrowDownLeft, ArrowUpRight, History, Wallet } from 'lucide-react';
interface Transaction {
id: string;
type: 'credit' | 'debit';
amount: number;
type: 'deposit' | 'withdrawal' | 'sponsorship' | 'prize';
formattedAmount: string;
description: string;
date: string;
formattedDate: string;
}
interface WalletSummaryPanelProps {
balance: number;
formattedBalance: string;
currency: string;
transactions: Transaction[];
onDeposit: () => void;
onWithdraw: () => void;
}
export function WalletSummaryPanel({ balance, currency, transactions, onDeposit, onWithdraw }: WalletSummaryPanelProps) {
export function WalletSummaryPanel({ formattedBalance, currency, transactions, onDeposit, onWithdraw }: WalletSummaryPanelProps) {
return (
<Stack gap={6}>
<Surface variant="dark" border rounded="lg" padding={8} position="relative" overflow="hidden">
@@ -48,7 +48,7 @@ export function WalletSummaryPanel({ balance, currency, transactions, onDeposit,
</Stack>
<Stack direction="row" align="baseline" gap={2}>
<Text size="4xl" weight="bold" color="text-white">
{balance.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
{formattedBalance}
</Text>
<Text size="xl" weight="medium" color="text-gray-500">{currency}</Text>
</Stack>
@@ -87,37 +87,40 @@ export function WalletSummaryPanel({ balance, currency, transactions, onDeposit,
<Text color="text-gray-500">No recent transactions.</Text>
</Stack>
) : (
transactions.map((tx) => (
<Stack key={tx.id} p={4} borderBottom borderColor="border-charcoal-outline" hoverBg="bg-white/5" transition>
<Stack direction="row" justify="between" align="center">
<Stack direction="row" align="center" gap={4}>
<Stack
center
w={10}
h={10}
rounded="full"
bg={tx.type === 'credit' ? 'bg-performance-green/10' : 'bg-error-red/10'}
transactions.map((tx) => {
const isCredit = tx.type === 'deposit' || tx.type === 'sponsorship';
return (
<Stack key={tx.id} p={4} borderBottom borderColor="border-charcoal-outline" hoverBg="bg-white/5" transition>
<Stack direction="row" justify="between" align="center">
<Stack direction="row" align="center" gap={4}>
<Stack
center
w={10}
h={10}
rounded="full"
bg={isCredit ? 'bg-performance-green/10' : 'bg-error-red/10'}
>
<Icon
icon={isCredit ? ArrowDownLeft : ArrowUpRight}
size={4}
color={isCredit ? 'text-performance-green' : 'text-error-red'}
/>
</Stack>
<Stack gap={0.5}>
<Text weight="medium" color="text-white">{tx.description}</Text>
<Text size="xs" color="text-gray-500">{tx.formattedDate}</Text>
</Stack>
</Stack>
<Text
weight="bold"
color={isCredit ? 'text-performance-green' : 'text-white'}
>
<Icon
icon={tx.type === 'credit' ? ArrowDownLeft : ArrowUpRight}
size={4}
color={tx.type === 'credit' ? 'text-performance-green' : 'text-error-red'}
/>
</Stack>
<Stack gap={0.5}>
<Text weight="medium" color="text-white">{tx.description}</Text>
<Text size="xs" color="text-gray-500">{new Date(tx.date).toLocaleDateString()}</Text>
</Stack>
{tx.formattedAmount}
</Text>
</Stack>
<Text
weight="bold"
color={tx.type === 'credit' ? 'text-performance-green' : 'text-white'}
>
{tx.type === 'credit' ? '+' : '-'}{tx.amount.toFixed(2)}
</Text>
</Stack>
</Stack>
))
);
})
)}
</Stack>
</Surface>

View File

@@ -19,12 +19,12 @@ interface ProfileHeaderProps {
avatarUrl?: string;
country: string;
iracingId: number;
joinedAt: string | Date;
joinedAtLabel: string;
};
stats: {
rating: number;
ratingLabel: string;
} | null;
globalRank: number;
globalRankLabel: string;
onAddFriend?: () => void;
friendRequestSent?: boolean;
isOwnProfile?: boolean;
@@ -33,7 +33,7 @@ interface ProfileHeaderProps {
export function ProfileHeader({
driver,
stats,
globalRank,
globalRankLabel,
onAddFriend,
friendRequestSent,
isOwnProfile,
@@ -69,7 +69,7 @@ export function ProfileHeader({
<Group gap={1.5}>
<Calendar size={14} color="var(--ui-color-text-low)" />
<Text size="xs" variant="low">
Joined {new Date(driver.joinedAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
Joined {driver.joinedAtLabel}
</Text>
</Group>
</Group>
@@ -78,8 +78,8 @@ export function ProfileHeader({
<ProfileStatsGroup>
{stats && (
<React.Fragment>
<ProfileStat label="RATING" value={stats.rating} intent="primary" />
<ProfileStat label="GLOBAL RANK" value={`#${globalRank}`} intent="warning" />
<ProfileStat label="RATING" value={stats.ratingLabel} intent="primary" />
<ProfileStat label="GLOBAL RANK" value={globalRankLabel} intent="warning" />
</React.Fragment>
)}
</ProfileStatsGroup>

View File

@@ -9,7 +9,7 @@ type RaceWithResults = {
track: string;
car: string;
winnerName: string;
scheduledAt: string | Date;
formattedDate: string;
};
interface LatestResultsSidebarProps {
@@ -28,14 +28,12 @@ export function LatestResultsSidebar({ results }: LatestResultsSidebarProps) {
</Heading>
<RaceResultList>
{results.slice(0, 4).map((result) => {
const scheduledAt = typeof result.scheduledAt === 'string' ? new Date(result.scheduledAt) : result.scheduledAt;
return (
<Box as="li" key={result.raceId}>
<RaceSummaryItem
track={result.track}
meta={`${result.winnerName}${result.car}`}
dateLabel={scheduledAt.toLocaleDateString()}
dateLabel={result.formattedDate}
/>
</Box>
);

View File

@@ -10,7 +10,8 @@ import { Calendar, Car, Clock, LucideIcon } from 'lucide-react';
interface RaceHeroProps {
track: string;
scheduledAt: string;
formattedDate: string;
formattedTime: string;
car: string;
status: 'scheduled' | 'running' | 'completed' | 'cancelled';
statusConfig: {
@@ -20,9 +21,8 @@ interface RaceHeroProps {
};
}
export function RaceHero({ track, scheduledAt, car, status, statusConfig }: RaceHeroProps) {
export function RaceHero({ track, formattedDate, formattedTime, car, status, statusConfig }: RaceHeroProps) {
const StatusIcon = statusConfig.icon;
const date = new Date(scheduledAt);
return (
<Hero variant="primary">
@@ -59,11 +59,11 @@ export function RaceHero({ track, scheduledAt, car, status, statusConfig }: Race
<Stack direction="row" align="center" gap={6} wrap>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Calendar} size={4} color="rgb(156, 163, 175)" />
<Text color="text-gray-400">{date.toLocaleDateString()}</Text>
<Text color="text-gray-400">{formattedDate}</Text>
</Stack>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Clock} size={4} color="rgb(156, 163, 175)" />
<Text color="text-gray-400">{date.toLocaleTimeString()}</Text>
<Text color="text-gray-400">{formattedTime}</Text>
</Stack>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Car} size={4} color="rgb(156, 163, 175)" />

View File

@@ -2,6 +2,7 @@
import { RaceHero as UiRaceHero } from '@/components/races/RaceHero';
import { LucideIcon } from 'lucide-react';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
interface RaceHeroProps {
track: string;
@@ -17,7 +18,7 @@ interface RaceHeroProps {
}
export function RaceHero(props: RaceHeroProps) {
const { statusConfig, ...rest } = props;
const { statusConfig, scheduledAt, ...rest } = props;
// Map variant to match UI component expectations
const mappedConfig: {
@@ -30,5 +31,12 @@ export function RaceHero(props: RaceHeroProps) {
variant: statusConfig.variant === 'default' ? 'default' : statusConfig.variant
};
return <UiRaceHero {...rest} statusConfig={mappedConfig} />;
return (
<UiRaceHero
{...rest}
formattedDate={DateDisplay.formatShort(scheduledAt)}
formattedTime={DateDisplay.formatTime(scheduledAt)}
statusConfig={mappedConfig}
/>
);
}

View File

@@ -2,7 +2,8 @@
import { routes } from '@/lib/routing/RouteConfig';
import { RaceListItem as UiRaceListItem } from '@/components/races/RaceListItem';
import { CheckCircle2, Clock, PlayCircle, XCircle } from 'lucide-react';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import { StatusDisplay } from '@/lib/display-objects/StatusDisplay';
interface Race {
id: string;
@@ -26,45 +27,32 @@ export function RaceListItem({ race, onClick }: RaceListItemProps) {
scheduled: {
iconName: 'Clock',
variant: 'primary' as const,
label: 'Scheduled',
},
running: {
iconName: 'PlayCircle',
variant: 'success' as const,
label: 'LIVE',
},
completed: {
iconName: 'CheckCircle2',
variant: 'default' as const,
label: 'Completed',
},
cancelled: {
iconName: 'XCircle',
variant: 'warning' as const,
label: 'Cancelled',
},
};
const config = statusConfig[race.status];
const formatTime = (date: string) => {
return new Date(date).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
});
};
const date = new Date(race.scheduledAt);
return (
<UiRaceListItem
track={race.track}
car={race.car}
dateLabel={date.toLocaleDateString('en-US', { month: 'short' })}
dayLabel={date.getDate().toString()}
timeLabel={formatTime(race.scheduledAt)}
dateLabel={DateDisplay.formatMonthDay(race.scheduledAt).split(' ')[0]}
dayLabel={DateDisplay.formatMonthDay(race.scheduledAt).split(' ')[1]}
timeLabel={DateDisplay.formatTime(race.scheduledAt)}
status={race.status}
statusLabel={config.label}
statusLabel={StatusDisplay.raceStatus(race.status)}
statusVariant={config.variant}
statusIconName={config.iconName}
leagueName={race.leagueName}

View File

@@ -12,10 +12,12 @@ interface RaceResultCardProps {
raceId: string;
track: string;
car: string;
scheduledAt: string | Date;
formattedDate: string;
position: number;
startPosition: number;
incidents: number;
positionLabel: string;
startPositionLabel: string;
incidentsLabel: string;
positionsGainedLabel?: string;
leagueName?: string;
showLeague?: boolean;
onClick?: () => void;
@@ -25,10 +27,12 @@ export function RaceResultCard({
raceId,
track,
car,
scheduledAt,
formattedDate,
position,
startPosition,
incidents,
positionLabel,
startPositionLabel,
incidentsLabel,
positionsGainedLabel,
leagueName,
showLeague = true,
onClick,
@@ -66,7 +70,7 @@ export function RaceResultCard({
border
borderColor="border-outline-steel"
>
P{position}
{positionLabel}
</Stack>
<Stack>
<Text color="text-white" weight="medium" block groupHoverTextColor="text-primary-accent" transition>
@@ -78,11 +82,7 @@ export function RaceResultCard({
<Stack direction="row" align="center" gap={3}>
<Stack textAlign="right">
<Text size="sm" color="text-gray-400" block>
{new Date(scheduledAt).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
{formattedDate}
</Text>
{showLeague && leagueName && (
<Text size="xs" color="text-gray-500" block>{leagueName}</Text>
@@ -92,16 +92,16 @@ export function RaceResultCard({
</Stack>
</Stack>
<Stack direction="row" align="center" gap={4}>
<Text size="xs" color="text-gray-500">Started P{startPosition}</Text>
<Text size="xs" color="text-gray-500">Started {startPositionLabel}</Text>
<Text size="xs" color="text-gray-500"></Text>
<Text size="xs" color={incidents === 0 ? 'text-success-green' : incidents > 2 ? 'text-error-red' : 'text-gray-500'}>
{incidents}x incidents
<Text size="xs" color="text-gray-500">
{incidentsLabel}
</Text>
{position < startPosition && (
{positionsGainedLabel && (
<>
<Text size="xs" color="text-gray-500"></Text>
<Text size="xs" color="text-success-green">
+{startPosition - position} positions
{positionsGainedLabel}
</Text>
</>
)}

View File

@@ -2,6 +2,7 @@
import { RaceResultViewModel } from '@/lib/view-models/RaceResultViewModel';
import { RaceResultCard as UiRaceResultCard } from './RaceResultCard';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
interface RaceResultCardProps {
race: {
@@ -28,10 +29,12 @@ export function RaceResultCard({
raceId={race.id}
track={race.track}
car={race.car}
scheduledAt={race.scheduledAt}
formattedDate={DateDisplay.formatShort(race.scheduledAt)}
position={result.position}
startPosition={result.startPosition}
incidents={result.incidents}
positionLabel={result.formattedPosition}
startPositionLabel={result.formattedStartPosition}
incidentsLabel={result.formattedIncidents}
positionsGainedLabel={result.formattedPositionsGained}
leagueName={league?.name}
showLeague={showLeague}
/>

View File

@@ -8,7 +8,7 @@ type UpcomingRace = {
id: string;
track: string;
car: string;
scheduledAt: string | Date;
formattedDate: string;
};
interface UpcomingRacesSidebarProps {
@@ -35,14 +35,12 @@ export function UpcomingRacesSidebar({ races }: UpcomingRacesSidebarProps) {
</Stack>
<Stack gap={3}>
{races.slice(0, 4).map((race) => {
const scheduledAt = typeof race.scheduledAt === 'string' ? new Date(race.scheduledAt) : race.scheduledAt;
return (
<RaceSummaryItem
key={race.id}
track={race.track}
meta={race.car}
dateLabel={scheduledAt.toLocaleDateString()}
dateLabel={race.formattedDate}
/>
);
})}

View File

@@ -8,6 +8,7 @@ export interface PricingTier {
id: string;
name: string;
price: number;
priceLabel: string;
period: string;
description: string;
features: string[];
@@ -69,7 +70,7 @@ export function PricingTableShell({ title, tiers, onSelect, selectedId }: Pricin
{tier.name}
</Text>
<Stack direction="row" align="baseline" gap={1}>
<Text size="3xl" weight="bold" color="text-white">${tier.price}</Text>
<Text size="3xl" weight="bold" color="text-white">{tier.priceLabel}</Text>
<Text size="sm" color="text-gray-500">/{tier.period}</Text>
</Stack>
<Text size="sm" color="text-gray-400" block mt={2}>

View File

@@ -1,62 +1,61 @@
export interface SponsorshipSlot {
tier: 'main' | 'secondary';
available: boolean;
price: number;
currency?: string;
priceLabel: string;
benefits: string[];
}
export const SlotTemplates = {
league: (mainAvailable: boolean, secondaryAvailable: number, mainPrice: number, secondaryPrice: number): SponsorshipSlot[] => [
league: (mainAvailable: boolean, secondaryAvailable: number, mainPriceLabel: string, secondaryPriceLabel: string): SponsorshipSlot[] => [
{
tier: 'main',
available: mainAvailable,
price: mainPrice,
priceLabel: mainPriceLabel,
benefits: ['Hood placement', 'League banner', 'Prominent logo'],
},
{
tier: 'secondary',
available: secondaryAvailable > 0,
price: secondaryPrice,
priceLabel: secondaryPriceLabel,
benefits: ['Side logo placement', 'League page listing'],
},
{
tier: 'secondary',
available: secondaryAvailable > 1,
price: secondaryPrice,
priceLabel: secondaryPriceLabel,
benefits: ['Side logo placement', 'League page listing'],
},
],
race: (mainAvailable: boolean, mainPrice: number): SponsorshipSlot[] => [
race: (mainAvailable: boolean, mainPriceLabel: string): SponsorshipSlot[] => [
{
tier: 'main',
available: mainAvailable,
price: mainPrice,
priceLabel: mainPriceLabel,
benefits: ['Race title sponsor', 'Stream overlay', 'Results banner'],
},
],
driver: (available: boolean, price: number): SponsorshipSlot[] => [
driver: (available: boolean, priceLabel: string): SponsorshipSlot[] => [
{
tier: 'main',
available,
price,
priceLabel,
benefits: ['Suit logo', 'Helmet branding', 'Social mentions'],
},
],
team: (mainAvailable: boolean, secondaryAvailable: boolean, mainPrice: number, secondaryPrice: number): SponsorshipSlot[] => [
team: (mainAvailable: boolean, secondaryAvailable: boolean, mainPriceLabel: string, secondaryPriceLabel: string): SponsorshipSlot[] => [
{
tier: 'main',
available: mainAvailable,
price: mainPrice,
priceLabel: mainPriceLabel,
benefits: ['Team name suffix', 'Car livery', 'All driver suits'],
},
{
tier: 'secondary',
available: secondaryAvailable,
price: secondaryPrice,
priceLabel: secondaryPriceLabel,
benefits: ['Team page logo', 'Minor livery placement'],
},
],

View File

@@ -47,11 +47,11 @@ export interface SponsorInsightsProps {
slots: SponsorshipSlot[];
additionalStats?: {
label: string;
items: Array<{ label: string; value: string | number }>;
items: Array<{ label: string; value: string }>;
};
trustScore?: number;
discordMembers?: number;
monthlyActivity?: number;
trustScoreLabel?: string;
discordMembersLabel?: string;
monthlyActivityLabel?: string;
ctaLabel?: string;
ctaHref?: string;
currentSponsorId?: string;
@@ -67,9 +67,9 @@ export function SponsorInsightsCard({
metrics,
slots,
additionalStats,
trustScore,
discordMembers,
monthlyActivity,
trustScoreLabel,
discordMembersLabel,
monthlyActivityLabel,
ctaLabel,
ctaHref,
currentSponsorId,
@@ -111,22 +111,10 @@ export function SponsorInsightsCard({
setError(null);
try {
const slot = slotTier === 'main' ? mainSlot : secondarySlots[0];
const slotPrice = slot?.price ?? 0;
const request = {
sponsorId: currentSponsorId,
entityType: getSponsorableEntityType(entityType),
entityId,
tier: slotTier,
offeredAmount: slotPrice * 100,
currency: (slot?.currency as 'USD' | 'EUR' | 'GBP') ?? 'USD',
message: `Interested in sponsoring ${entityName} as ${slotTier} sponsor.`,
};
console.log('Sponsorship request:', request);
setAppliedTiers(prev => new Set([...prev, slotTier]));
// Note: In a real app, we would fetch the raw price from the API or a ViewModel
// For now, we assume the parent handles the actual request logic
onSponsorshipRequested?.(slotTier);
setAppliedTiers(prev => new Set([...prev, slotTier]));
} catch (err) {
console.error('Failed to apply for sponsorship:', err);
@@ -134,7 +122,7 @@ export function SponsorInsightsCard({
} finally {
setApplyingTier(null);
}
}, [currentSponsorId, ctaHref, entityType, entityId, entityName, onNavigate, mainSlot, secondarySlots, appliedTiers, getSponsorableEntityType, onSponsorshipRequested]);
}, [currentSponsorId, ctaHref, entityType, entityId, onNavigate, appliedTiers, onSponsorshipRequested]);
return (
<Card
@@ -171,27 +159,27 @@ export function SponsorInsightsCard({
})}
</Stack>
{(trustScore !== undefined || discordMembers !== undefined || monthlyActivity !== undefined) && (
{(trustScoreLabel || discordMembersLabel || monthlyActivityLabel) && (
<Stack display="flex" flexWrap="wrap" gap={4} mb={4} pb={4} borderBottom borderColor="border-charcoal-outline/50">
{trustScore !== undefined && (
{trustScoreLabel && (
<Stack direction="row" align="center" gap={2}>
<Icon icon={Shield} size={4} color="rgb(16, 185, 129)" />
<Text size="sm" color="text-gray-400">Trust Score:</Text>
<Text size="sm" weight="semibold" color="text-white">{trustScore}/100</Text>
<Text size="sm" weight="semibold" color="text-white">{trustScoreLabel}</Text>
</Stack>
)}
{discordMembers !== undefined && (
{discordMembersLabel && (
<Stack direction="row" align="center" gap={2}>
<Icon icon={MessageCircle} size={4} color="rgb(168, 85, 247)" />
<Text size="sm" color="text-gray-400">Discord:</Text>
<Text size="sm" weight="semibold" color="text-white">{discordMembers.toLocaleString()}</Text>
<Text size="sm" weight="semibold" color="text-white">{discordMembersLabel}</Text>
</Stack>
)}
{monthlyActivity !== undefined && (
{monthlyActivityLabel && (
<Stack direction="row" align="center" gap={2}>
<Icon icon={Activity} size={4} color="rgb(0, 255, 255)" />
<Text size="sm" color="text-gray-400">Monthly Activity:</Text>
<Text size="sm" weight="semibold" color="text-white">{monthlyActivity}%</Text>
<Text size="sm" weight="semibold" color="text-white">{monthlyActivityLabel}</Text>
</Stack>
)}
</Stack>
@@ -206,7 +194,7 @@ export function SponsorInsightsCard({
statusColor={mainSlot.available ? 'text-performance-green' : 'text-gray-500'}
benefits={mainSlot.benefits.join(' • ')}
available={mainSlot.available}
price={`$${mainSlot.price.toLocaleString()}/season`}
price={mainSlot.priceLabel}
action={
<Button
variant="primary"
@@ -240,7 +228,7 @@ export function SponsorInsightsCard({
statusColor={availableSecondary > 0 ? 'text-purple-400' : 'text-gray-500'}
benefits={secondarySlots[0]?.benefits.join(' • ') || 'Logo placement on page'}
available={availableSecondary > 0}
price={`$${secondarySlots[0]?.price.toLocaleString()}/season`}
price={secondarySlots[0]?.priceLabel}
action={
<Button
variant="secondary"
@@ -275,7 +263,7 @@ export function SponsorInsightsCard({
<Stack key={index} direction="row" align="center" gap={2}>
<Text size="sm" color="text-gray-500">{item.label}:</Text>
<Text size="sm" weight="semibold" color="text-white">
{typeof item.value === 'number' ? item.value.toLocaleString() : item.value}
{item.value}
</Text>
</Stack>
))}

View File

@@ -3,10 +3,10 @@ import { ComponentType } from 'react';
export interface SponsorMetric {
icon: string | ComponentType;
label: string;
value: string | number;
value: string;
color?: string;
trend?: {
value: number;
value: string;
isPositive: boolean;
};
}
@@ -14,7 +14,6 @@ export interface SponsorMetric {
export interface SponsorshipSlot {
tier: 'main' | 'secondary';
available: boolean;
price: number;
currency?: string;
priceLabel: string;
benefits: string[];
}

View File

@@ -5,11 +5,11 @@ import { LucideIcon } from 'lucide-react';
interface SponsorMetricCardProps {
label: string;
value: string | number;
value: string;
icon: LucideIcon;
color?: string;
trend?: {
value: number;
value: string;
isPositive: boolean;
};
}
@@ -35,11 +35,11 @@ export function SponsorMetricCard({
</Box>
<Box display="flex" alignItems="baseline" gap={2}>
<Text size="xl" weight="bold" color="text-white">
{typeof value === 'number' ? value.toLocaleString() : value}
{value}
</Text>
{trend && (
<Text size="xs" color={trend.isPositive ? 'text-performance-green' : 'text-red-400'}>
{trend.isPositive ? '+' : ''}{trend.value}%
{trend.value}
</Text>
)}
</Box>

View File

@@ -8,8 +8,8 @@ import { LucideIcon } from 'lucide-react';
interface SponsorshipCategoryCardProps {
icon: LucideIcon;
title: string;
count: number;
impressions: number;
countLabel: string;
impressionsLabel: string;
color: string;
href: string;
}
@@ -17,8 +17,8 @@ interface SponsorshipCategoryCardProps {
export function SponsorshipCategoryCard({
icon,
title,
count,
impressions,
countLabel,
impressionsLabel,
color,
href
}: SponsorshipCategoryCardProps) {
@@ -39,11 +39,11 @@ export function SponsorshipCategoryCard({
</Stack>
<Stack>
<Text weight="medium" color="text-white" block>{title}</Text>
<Text size="sm" color="text-gray-500">{count} active</Text>
<Text size="sm" color="text-gray-500">{countLabel}</Text>
</Stack>
</Stack>
<Stack textAlign="right">
<Text weight="semibold" color="text-white" block>{impressions.toLocaleString()}</Text>
<Text weight="semibold" color="text-white" block>{impressionsLabel}</Text>
<Text size="xs" color="text-gray-500">impressions</Text>
</Stack>
</Stack>

View File

@@ -7,9 +7,9 @@ import { AlertTriangle, Check, Clock, Download, Receipt } from 'lucide-react';
export interface Transaction {
id: string;
date: string;
formattedDate: string;
description: string;
amount: number;
formattedAmount: string;
status: 'paid' | 'pending' | 'overdue' | 'failed';
invoiceNumber: string;
type: string;
@@ -103,11 +103,11 @@ export function TransactionTable({ transactions, onDownload }: TransactionTableP
</Stack>
<Stack colSpan={{ base: 1, md: 2 } as any}>
<Text size="sm" color="text-gray-400">{new Date(tx.date).toLocaleDateString()}</Text>
<Text size="sm" color="text-gray-400">{tx.formattedDate}</Text>
</Stack>
<Stack colSpan={{ base: 1, md: 2 } as any}>
<Text weight="semibold" color="text-white">${tx.amount.toFixed(2)}</Text>
<Text weight="semibold" color="text-white">{tx.formattedAmount}</Text>
</Stack>
<Stack colSpan={{ base: 1, md: 2 } as any}>

View File

@@ -28,9 +28,9 @@ interface SkillLevelSectionProps {
description?: string;
logoUrl?: string;
memberCount: number;
rating?: number;
totalWins: number;
totalRaces: number;
ratingLabel: string;
winsLabel: string;
racesLabel: string;
performanceLevel: string;
isRecruiting: boolean;
specialization?: string;
@@ -86,9 +86,9 @@ export function SkillLevelSection({
description={team.description ?? ''}
logo={team.logoUrl}
memberCount={team.memberCount}
rating={team.rating}
totalWins={team.totalWins}
totalRaces={team.totalRaces}
ratingLabel={team.ratingLabel}
winsLabel={team.winsLabel}
racesLabel={team.racesLabel}
performanceLevel={team.performanceLevel as SkillLevel}
isRecruiting={team.isRecruiting}
specialization={specialization(team.specialization)}

View File

@@ -21,9 +21,9 @@ interface TeamCardProps {
description?: string;
logo?: string;
memberCount: number;
rating?: number | null;
totalWins?: number;
totalRaces?: number;
ratingLabel: string;
winsLabel: string;
racesLabel: string;
performanceLevel?: 'beginner' | 'intermediate' | 'advanced' | 'pro';
isRecruiting?: boolean;
specialization?: 'endurance' | 'sprint' | 'mixed' | undefined;
@@ -65,9 +65,9 @@ export function TeamCard({
description,
logo,
memberCount,
rating,
totalWins,
totalRaces,
ratingLabel,
winsLabel,
racesLabel,
performanceLevel,
isRecruiting,
specialization,
@@ -112,9 +112,9 @@ export function TeamCard({
)}
statsContent={
<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" />
<TeamStatItem label="Rating" value={ratingLabel} intent="primary" align="center" />
<TeamStatItem label="Wins" value={winsLabel} intent="success" align="center" />
<TeamStatItem label="Races" value={racesLabel} intent="high" align="center" />
</Group>
}
/>

View File

@@ -11,10 +11,10 @@ interface TeamLeaderboardItemProps {
name: string;
logoUrl: string;
category?: string;
memberCount: number;
totalWins: number;
memberCountLabel: string;
totalWinsLabel: string;
isRecruiting: boolean;
rating?: number;
ratingLabel: string;
onClick?: () => void;
medalColor: string;
medalBg: string;
@@ -26,10 +26,10 @@ export function TeamLeaderboardItem({
name,
logoUrl,
category,
memberCount,
totalWins,
memberCountLabel,
totalWinsLabel,
isRecruiting,
rating,
ratingLabel,
onClick,
medalColor,
medalBg,
@@ -99,11 +99,11 @@ export function TeamLeaderboardItem({
)}
<Stack direction="row" align="center" gap={1}>
<Icon icon={Users} size={3} color="var(--text-gray-600)" />
<Text size="xs" color="text-gray-500">{memberCount}</Text>
<Text size="xs" color="text-gray-500">{memberCountLabel}</Text>
</Stack>
<Stack direction="row" align="center" gap={1}>
<Icon icon={Trophy} size={3} color="var(--text-gray-600)" />
<Text size="xs" color="text-gray-500">{totalWins} wins</Text>
<Text size="xs" color="text-gray-500">{totalWinsLabel}</Text>
</Stack>
{isRecruiting && (
<Stack direction="row" align="center" gap={1}>
@@ -117,7 +117,7 @@ export function TeamLeaderboardItem({
{/* Rating */}
<Stack textAlign="right">
<Text font="mono" weight="semibold" color="text-purple-400" block>
{typeof rating === 'number' ? Math.round(rating).toLocaleString() : '—'}
{ratingLabel}
</Text>
<Text size="xs" color="text-gray-500">Rating</Text>
</Stack>

View File

@@ -9,10 +9,10 @@ interface TeamLeaderboardPanelProps {
id: string;
name: string;
logoUrl?: string;
rating: number;
wins: number;
races: number;
memberCount: number;
ratingLabel: string;
winsLabel: string;
racesLabel: string;
memberCountLabel: string;
}>;
onTeamClick: (id: string) => void;
}
@@ -53,16 +53,16 @@ export function TeamLeaderboardPanel({ teams, onTeamClick }: TeamLeaderboardPane
</Stack>
</TableCell>
<TableCell className="text-center">
<Text font="mono" weight="bold" color="text-primary-blue">{team.rating}</Text>
<Text font="mono" weight="bold" color="text-primary-blue">{team.ratingLabel}</Text>
</TableCell>
<TableCell className="text-center">
<Text font="mono" color="text-gray-300">{team.wins}</Text>
<Text font="mono" color="text-gray-300">{team.winsLabel}</Text>
</TableCell>
<TableCell className="text-center">
<Text font="mono" color="text-gray-300">{team.races}</Text>
<Text font="mono" color="text-gray-300">{team.racesLabel}</Text>
</TableCell>
<TableCell className="text-center">
<Text font="mono" color="text-gray-400" size="xs">{team.memberCount}</Text>
<Text font="mono" color="text-gray-400" size="xs">{team.memberCountLabel}</Text>
</TableCell>
</TableRow>
))}

View File

@@ -11,14 +11,14 @@ import { ChevronRight, Users } from 'lucide-react';
interface TeamMembershipCardProps {
teamName: string;
role: string;
joinedAt: string;
joinedAtLabel: string;
href: string;
}
export function TeamMembershipCard({
teamName,
role,
joinedAt,
joinedAtLabel,
href,
}: TeamMembershipCardProps) {
return (
@@ -52,7 +52,7 @@ export function TeamMembershipCard({
{role}
</Badge>
<Text size="xs" color="text-gray-400">
Since {new Date(joinedAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
Since {joinedAtLabel}
</Text>
</Stack>
</Stack>

View File

@@ -13,7 +13,7 @@ interface TeamMembership {
name: string;
};
role: string;
joinedAt: Date;
joinedAtLabel: string;
}
interface TeamMembershipGridProps {
@@ -46,7 +46,7 @@ export function TeamMembershipGrid({ memberships }: TeamMembershipGridProps) {
<Surface variant="muted" rounded="full" padding={1} style={{ paddingLeft: '0.5rem', paddingRight: '0.5rem', backgroundColor: 'rgba(147, 51, 234, 0.2)', color: '#a855f7' }}>
<Text size="xs" weight="medium" style={{ textTransform: 'capitalize' }}>{membership.role}</Text>
</Surface>
<Text size="xs" color="text-gray-500">Since {membership.joinedAt.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}</Text>
<Text size="xs" color="text-gray-400">Since {membership.joinedAtLabel}</Text>
</Stack>
</Stack>
<ChevronRight style={{ width: '1rem', height: '1rem', color: '#737373' }} />

View File

@@ -15,6 +15,10 @@ import { Select } from '@/ui/Select';
import { Text } from '@/ui/Text';
import { useMemo, useState } from 'react';
import { MemberDisplay } from '@/lib/display-objects/MemberDisplay';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
export type TeamRole = 'owner' | 'admin' | 'member';
export type TeamMemberRole = 'owner' | 'manager' | 'member';
@@ -67,9 +71,10 @@ export function TeamRoster({
return sortMembers(teamMembers as unknown as TeamMember[], sortBy);
}, [teamMembers, sortBy]);
const teamAverageRating = useMemo(() => {
if (teamMembers.length === 0) return 0;
return teamMembers.reduce((sum: number, m: { rating?: number | null }) => sum + (m.rating || 0), 0) / teamMembers.length;
const teamAverageRatingLabel = useMemo(() => {
if (teamMembers.length === 0) return '—';
const avg = teamMembers.reduce((sum: number, m: { rating?: number | null }) => sum + (m.rating || 0), 0) / teamMembers.length;
return RatingDisplay.format(avg);
}, [teamMembers]);
if (loading) {
@@ -88,8 +93,8 @@ export function TeamRoster({
<Stack>
<Heading level={3}>Team Roster</Heading>
<Text size="sm" color="text-gray-400" block mt={1}>
{memberships.length} {memberships.length === 1 ? 'member' : 'members'} Avg Rating:{' '}
<Text color="text-primary-blue" weight="medium">{teamAverageRating.toFixed(0)}</Text>
{MemberDisplay.formatCount(memberships.length)} Avg Rating:{' '}
<Text color="text-primary-blue" weight="medium">{teamAverageRatingLabel}</Text>
</Text>
</Stack>
@@ -124,9 +129,9 @@ export function TeamRoster({
driver={driver as DriverViewModel}
href={`${routes.driver.detail(driver.id)}?from=team&teamId=${teamId}`}
roleLabel={getRoleLabel(role)}
joinedAt={joinedAt}
rating={rating}
overallRank={overallRank}
joinedAtLabel={DateDisplay.formatShort(joinedAt)}
ratingLabel={RatingDisplay.format(rating)}
overallRankLabel={overallRank !== null ? `#${overallRank}` : null}
actions={canManageMembership ? (
<>
<Stack width="32">

View File

@@ -8,9 +8,9 @@ interface TeamRosterItemProps {
driver: DriverViewModel;
href: string;
roleLabel: string;
joinedAt: string | Date;
rating: number | null;
overallRank: number | null;
joinedAtLabel: string;
ratingLabel: string | null;
overallRankLabel: string | null;
actions?: ReactNode;
}
@@ -18,9 +18,9 @@ export function TeamRosterItem({
driver,
href,
roleLabel,
joinedAt,
rating,
overallRank,
joinedAtLabel,
ratingLabel,
overallRankLabel,
actions,
}: TeamRosterItemProps) {
return (
@@ -38,23 +38,23 @@ export function TeamRosterItem({
contextLabel={roleLabel}
meta={
<Text size="xs" color="text-gray-400">
{driver.country} Joined {new Date(joinedAt).toLocaleDateString()}
{driver.country} Joined {joinedAtLabel}
</Text>
}
size="md"
/>
{rating !== null && (
{ratingLabel !== null && (
<Stack direction="row" align="center" gap={6}>
<Stack display="flex" flexDirection="col" alignItems="center">
<Text size="lg" weight="bold" color="text-primary-blue" block>
{rating}
{ratingLabel}
</Text>
<Text size="xs" color="text-gray-400">Rating</Text>
</Stack>
{overallRank !== null && (
{overallRankLabel !== null && (
<Stack display="flex" flexDirection="col" alignItems="center">
<Text size="sm" color="text-gray-300" block>#{overallRank}</Text>
<Text size="sm" color="text-gray-300" block>{overallRankLabel}</Text>
<Text size="xs" color="text-gray-500">Rank</Text>
</Stack>
)}