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

@@ -1,11 +1,15 @@
'use client'; 'use client';
import { useSponsorSponsorships } from "@/hooks/sponsor/useSponsorSponsorships"; import { useSponsorSponsorships } from "@/hooks/sponsor/useSponsorSponsorships";
import { SponsorCampaignsTemplate, SponsorshipType } from "@/templates/SponsorCampaignsTemplate"; import { SponsorCampaignsTemplate, SponsorshipType, SponsorCampaignsViewData } from "@/templates/SponsorCampaignsTemplate";
import { Box } from "@/ui/Box"; import { Box } from "@/ui/Box";
import { Button } from "@/ui/Button"; import { Button } from "@/ui/Button";
import { Text } from "@/ui/Text"; import { Text } from "@/ui/Text";
import { useState } from 'react'; import { useState } from 'react';
import { CurrencyDisplay } from "@/lib/display-objects/CurrencyDisplay";
import { NumberDisplay } from "@/lib/display-objects/NumberDisplay";
import { DateDisplay } from "@/lib/display-objects/DateDisplay";
import { StatusDisplay } from "@/lib/display-objects/StatusDisplay";
export default function SponsorCampaignsPage() { export default function SponsorCampaignsPage() {
const [typeFilter, setTypeFilter] = useState<SponsorshipType>('all'); const [typeFilter, setTypeFilter] = useState<SponsorshipType>('all');
@@ -39,22 +43,33 @@ export default function SponsorCampaignsPage() {
} }
// Calculate stats // Calculate stats
const totalInvestment = sponsorshipsData.sponsorships.filter((s: any) => s.status === 'active').reduce((sum: number, s: any) => sum + s.price, 0);
const totalImpressions = sponsorshipsData.sponsorships.reduce((sum: number, s: any) => sum + s.impressions, 0);
const stats = { const stats = {
total: sponsorshipsData.sponsorships.length, total: sponsorshipsData.sponsorships.length,
active: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'active').length, active: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'active').length,
pending: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'pending_approval').length, pending: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'pending_approval').length,
approved: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'approved').length, approved: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'approved').length,
rejected: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'rejected').length, rejected: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'rejected').length,
totalInvestment: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'active').reduce((sum: number, s: any) => sum + s.price, 0), formattedTotalInvestment: CurrencyDisplay.format(totalInvestment),
totalImpressions: sponsorshipsData.sponsorships.reduce((sum: number, s: any) => sum + s.impressions, 0), formattedTotalImpressions: NumberDisplay.formatCompact(totalImpressions),
}; };
const viewData = { const sponsorships = sponsorshipsData.sponsorships.map((s: any) => ({
sponsorships: sponsorshipsData.sponsorships as any, ...s,
stats, formattedInvestment: CurrencyDisplay.format(s.price),
formattedImpressions: NumberDisplay.format(s.impressions),
formattedStartDate: s.seasonStartDate ? DateDisplay.formatShort(s.seasonStartDate) : undefined,
formattedEndDate: s.seasonEndDate ? DateDisplay.formatShort(s.seasonEndDate) : undefined,
}));
const viewData: SponsorCampaignsViewData = {
sponsorships,
stats: stats as any,
}; };
const filteredSponsorships = sponsorshipsData.sponsorships.filter((s: any) => { const filteredSponsorships = sponsorships.filter((s: any) => {
// For now, we only have leagues in the DTO // For now, we only have leagues in the DTO
if (typeFilter !== 'all' && typeFilter !== 'leagues') return false; if (typeFilter !== 'all' && typeFilter !== 'leagues') return false;
if (searchQuery && !s.leagueName.toLowerCase().includes(searchQuery.toLowerCase())) return false; if (searchQuery && !s.leagueName.toLowerCase().includes(searchQuery.toLowerCase())) return false;

View File

@@ -17,6 +17,8 @@ import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRos
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO'; import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts'; import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
const ROLE_OPTIONS: MembershipRole[] = ['owner', 'admin', 'steward', 'member']; const ROLE_OPTIONS: MembershipRole[] = ['owner', 'admin', 'steward', 'member'];
export function RosterAdminPage({ viewData: initialViewData }: Partial<ClientWrapperProps<LeagueRosterAdminViewData>>) { export function RosterAdminPage({ viewData: initialViewData }: Partial<ClientWrapperProps<LeagueRosterAdminViewData>>) {
@@ -81,6 +83,7 @@ export function RosterAdminPage({ viewData: initialViewData }: Partial<ClientWra
id: req.id, id: req.id,
driver: req.driver as { id: string; name: string }, driver: req.driver as { id: string; name: string },
requestedAt: req.requestedAt, requestedAt: req.requestedAt,
formattedRequestedAt: DateDisplay.formatShort(req.requestedAt),
message: req.message || undefined, message: req.message || undefined,
})), })),
members: members.map((m: LeagueRosterMemberDTO): RosterMemberData => ({ members: members.map((m: LeagueRosterMemberDTO): RosterMemberData => ({
@@ -88,6 +91,7 @@ export function RosterAdminPage({ viewData: initialViewData }: Partial<ClientWra
driver: m.driver as { id: string; name: string }, driver: m.driver as { id: string; name: string },
role: m.role, role: m.role,
joinedAt: m.joinedAt, joinedAt: m.joinedAt,
formattedJoinedAt: DateDisplay.formatShort(m.joinedAt),
})), })),
}), [leagueId, joinRequests, members]); }), [leagueId, joinRequests, members]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,12 +17,12 @@ interface ProfileHeroProps {
avatarUrl?: string; avatarUrl?: string;
country: string; country: string;
iracingId: number; iracingId: number;
joinedAt: string | Date; joinedAtLabel: string;
}; };
stats: { stats: {
rating: number; ratingLabel: string;
} | null; } | null;
globalRank: number; globalRankLabel: string;
timezone: string; timezone: string;
socialHandles: { socialHandles: {
platform: string; platform: string;
@@ -47,7 +47,7 @@ function getSocialIcon(platform: string) {
export function ProfileHero({ export function ProfileHero({
driver, driver,
stats, stats,
globalRank, globalRankLabel,
timezone, timezone,
socialHandles, socialHandles,
onAddFriend, 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' }}> <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}> <Stack direction="row" align="center" gap={2}>
<Star style={{ width: '1rem', height: '1rem', color: '#3b82f6' }} /> <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> <Text size="xs" color="text-gray-400">Rating</Text>
</Stack> </Stack>
</Surface> </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' }}> <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}> <Stack direction="row" align="center" gap={2}>
<Trophy style={{ width: '1rem', height: '1rem', color: '#facc15' }} /> <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> <Text size="xs" color="text-gray-400">Global</Text>
</Stack> </Stack>
</Surface> </Surface>
@@ -111,11 +111,7 @@ export function ProfileHero({
<Stack direction="row" align="center" gap={1.5}> <Stack direction="row" align="center" gap={1.5}>
<Calendar style={{ width: '1rem', height: '1rem' }} /> <Calendar style={{ width: '1rem', height: '1rem' }} />
<Text size="sm"> <Text size="sm">
Joined{' '} Joined {driver.joinedAtLabel}
{new Date(driver.joinedAt).toLocaleDateString('en-US', {
month: 'short',
year: 'numeric',
})}
</Text> </Text>
</Stack> </Stack>
<Stack direction="row" align="center" gap={1.5}> <Stack direction="row" align="center" gap={1.5}>

View File

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

View File

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

View File

@@ -29,6 +29,12 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { useEffect, useState } from '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 { interface ErrorAnalyticsDashboardProps {
/** /**
* Auto-refresh interval in milliseconds * Auto-refresh interval in milliseconds
@@ -41,16 +47,16 @@ interface ErrorAnalyticsDashboardProps {
} }
function formatDuration(duration: number): string { function formatDuration(duration: number): string {
return duration.toFixed(2) + 'ms'; return DurationDisplay.formatMs(duration);
} }
function formatPercentage(value: number, total: number): string { function formatPercentage(value: number, total: number): string {
if (total === 0) return '0%'; if (total === 0) return '0%';
return ((value / total) * 100).toFixed(1) + '%'; return PercentDisplay.format(value / total);
} }
function formatMemory(bytes: number): string { function formatMemory(bytes: number): string {
return (bytes / 1024 / 1024).toFixed(1) + 'MB'; return MemoryDisplay.formatMB(bytes);
} }
interface PerformanceWithMemory extends Performance { interface PerformanceWithMemory extends Performance {
@@ -321,7 +327,7 @@ export function ErrorAnalyticsDashboard({
<Stack display="flex" justifyContent="between" alignItems="start" gap={2} mb={1}> <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" font="mono" weight="bold" color="text-red-400" truncate>{error.type}</Text>
<Text size="xs" color="text-gray-500" fontSize="10px"> <Text size="xs" color="text-gray-500" fontSize="10px">
{new Date(error.timestamp).toLocaleTimeString()} {DateDisplay.formatTime(error.timestamp)}
</Text> </Text>
</Stack> </Stack>
<Text size="xs" color="text-gray-300" block mb={1}>{error.message}</Text> <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 { Stack } from '@/ui/Stack';
import { Section } from '@/ui/Section'; import { Section } from '@/ui/Section';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
interface FeedItemData { interface FeedItemData {
id: string; id: string;
@@ -46,6 +47,16 @@ export function FeedLayout({
upcomingRaces, upcomingRaces,
latestResults latestResults
}: FeedLayoutProps) { }: FeedLayoutProps) {
const formattedUpcomingRaces = upcomingRaces.map(r => ({
...r,
formattedDate: DateDisplay.formatShort(r.scheduledAt),
}));
const formattedLatestResults = latestResults.map(r => ({
...r,
formattedDate: DateDisplay.formatShort(r.scheduledAt),
}));
return ( return (
<Section className="mt-16 mb-20"> <Section className="mt-16 mb-20">
<Container> <Container>
@@ -64,8 +75,8 @@ export function FeedLayout({
</Card> </Card>
</Stack> </Stack>
<Stack as="aside" gap={6}> <Stack as="aside" gap={6}>
<UpcomingRacesSidebar races={upcomingRaces} /> <UpcomingRacesSidebar races={formattedUpcomingRaces} />
<LatestResultsSidebar results={latestResults} /> <LatestResultsSidebar results={formattedLatestResults} />
</Stack> </Stack>
</Grid> </Grid>
</Container> </Container>

View File

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

View File

@@ -1,6 +1,7 @@
import { ActivityFeedItem } from '@/components/feed/ActivityFeedItem'; import { ActivityFeedItem } from '@/components/feed/ActivityFeedItem';
import { useLeagueRaces } from "@/hooks/league/useLeagueRaces"; import { useLeagueRaces } from "@/hooks/league/useLeagueRaces";
import { LeagueActivityService } from '@/lib/services/league/LeagueActivityService'; import { LeagueActivityService } from '@/lib/services/league/LeagueActivityService';
import { RelativeTimeDisplay } from '@/lib/display-objects/RelativeTimeDisplay';
import { Icon } from '@/ui/Icon'; import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
@@ -20,19 +21,6 @@ interface LeagueActivityFeedProps {
limit?: number; 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) { export function LeagueActivityFeed({ leagueId, limit = 10 }: LeagueActivityFeedProps) {
const { data: raceList = [], isLoading } = useLeagueRaces(leagueId); const { data: raceList = [], isLoading } = useLeagueRaces(leagueId);
@@ -140,7 +128,7 @@ function ActivityItem({ activity }: { activity: LeagueActivity }) {
<ActivityFeedItem <ActivityFeedItem
icon={getIcon()} icon={getIcon()}
content={getContent()} 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 { Card } from '@/ui/Card';
import { Grid } from '@/ui/Grid'; import { Grid } from '@/ui/Grid';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import { DurationDisplay } from '@/lib/display-objects/DurationDisplay';
interface LeagueReviewSummaryProps { interface LeagueReviewSummaryProps {
form: LeagueConfigFormModel; form: LeagueConfigFormModel;
presets: LeagueScoringPresetViewModel[]; presets: LeagueScoringPresetViewModel[];
@@ -139,11 +142,7 @@ export function LeagueReviewSummary({ form, presets }: LeagueReviewSummaryProps)
const seasonStartLabel = const seasonStartLabel =
timings.seasonStartDate timings.seasonStartDate
? new Date(timings.seasonStartDate).toLocaleDateString(undefined, { ? DateDisplay.formatShort(timings.seasonStartDate)
month: 'short',
day: 'numeric',
year: 'numeric',
})
: null; : null;
const stewardingLabel = (() => { const stewardingLabel = (() => {

View File

@@ -194,17 +194,10 @@ export function LeagueSchedule({ leagueId, onRaceClick }: LeagueScheduleProps) {
<Stack display="flex" alignItems="center" gap={3}> <Stack display="flex" alignItems="center" gap={3}>
<Stack textAlign="right"> <Stack textAlign="right">
<Text color="text-white" weight="medium" block> <Text color="text-white" weight="medium" block>
{race.scheduledAt.toLocaleDateString('en-US', { {race.formattedDate}
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</Text> </Text>
<Text size="sm" color="text-gray-400" block> <Text size="sm" color="text-gray-400" block>
{race.scheduledAt.toLocaleTimeString([], { {race.formattedTime}
hour: '2-digit',
minute: '2-digit',
})}
</Text> </Text>
{isPast && race.status === 'completed' && ( {isPast && race.status === 'completed' && (
<Text size="xs" color="text-primary-blue" mt={1} block>View Results </Text> <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"> <Stack direction="row" align="center" justify="between">
<Text size="sm" color="text-gray-400">Filed Date</Text> <Text size="sm" color="text-gray-400">Filed Date</Text>
<Text size="sm" color="text-white" weight="medium"> <Text size="sm" color="text-white" weight="medium">
{new Date(protest.filedAt || protest.submittedAt).toLocaleString()} {protest.formattedSubmittedAt}
</Text> </Text>
</Stack> </Stack>
<Stack direction="row" align="center" justify="between"> <Stack direction="row" align="center" justify="between">
@@ -299,7 +299,7 @@ export function ReviewProtestModal({
<Text size="sm" color="text-gray-400">Status</Text> <Text size="sm" color="text-gray-400">Status</Text>
<Stack as="span" px={2} py={1} rounded="sm" bg="bg-orange-500/20"> <Stack as="span" px={2} py={1} rounded="sm" bg="bg-orange-500/20">
<Text size="xs" weight="medium" color="text-orange-400"> <Text size="xs" weight="medium" color="text-orange-400">
{protest.status} {protest.statusDisplay}
</Text> </Text>
</Stack> </Stack>
</Stack> </Stack>

View File

@@ -15,8 +15,10 @@ interface Race {
name: string; name: string;
track?: string; track?: string;
car?: string; car?: string;
scheduledAt: string; formattedDate: string;
formattedTime: string;
status: string; status: string;
statusLabel: string;
sessionType?: string; sessionType?: string;
isPast?: boolean; 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'} /> <Stack w="3" h="3" rounded="full" bg={race.isPast ? 'bg-performance-green' : 'bg-primary-blue'} />
<Heading level={3} fontSize="lg">{race.name}</Heading> <Heading level={3} fontSize="lg">{race.name}</Heading>
<Badge variant={race.status === 'completed' ? 'success' : 'primary'}> <Badge variant={race.status === 'completed' ? 'success' : 'primary'}>
{race.status === 'completed' ? 'Completed' : 'Scheduled'} {race.statusLabel}
</Badge> </Badge>
</Stack> </Stack>
<Grid cols={4} gap={4}> <Grid cols={4} gap={4}>
<Stack direction="row" align="center" gap={2}> <Stack direction="row" align="center" gap={2}>
<Icon icon={Calendar} size={4} color="#9ca3af" /> <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>
<Stack direction="row" align="center" gap={2}> <Stack direction="row" align="center" gap={2}>
<Icon icon={Clock} size={4} color="#9ca3af" /> <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> </Stack>
{race.track && ( {race.track && (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,8 @@ import { Calendar, Car, Clock, LucideIcon } from 'lucide-react';
interface RaceHeroProps { interface RaceHeroProps {
track: string; track: string;
scheduledAt: string; formattedDate: string;
formattedTime: string;
car: string; car: string;
status: 'scheduled' | 'running' | 'completed' | 'cancelled'; status: 'scheduled' | 'running' | 'completed' | 'cancelled';
statusConfig: { 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 StatusIcon = statusConfig.icon;
const date = new Date(scheduledAt);
return ( return (
<Hero variant="primary"> <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={6} wrap>
<Stack direction="row" align="center" gap={2}> <Stack direction="row" align="center" gap={2}>
<Icon icon={Calendar} size={4} color="rgb(156, 163, 175)" /> <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>
<Stack direction="row" align="center" gap={2}> <Stack direction="row" align="center" gap={2}>
<Icon icon={Clock} size={4} color="rgb(156, 163, 175)" /> <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>
<Stack direction="row" align="center" gap={2}> <Stack direction="row" align="center" gap={2}>
<Icon icon={Car} size={4} color="rgb(156, 163, 175)" /> <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 { RaceHero as UiRaceHero } from '@/components/races/RaceHero';
import { LucideIcon } from 'lucide-react'; import { LucideIcon } from 'lucide-react';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
interface RaceHeroProps { interface RaceHeroProps {
track: string; track: string;
@@ -17,7 +18,7 @@ interface RaceHeroProps {
} }
export function RaceHero(props: RaceHeroProps) { export function RaceHero(props: RaceHeroProps) {
const { statusConfig, ...rest } = props; const { statusConfig, scheduledAt, ...rest } = props;
// Map variant to match UI component expectations // Map variant to match UI component expectations
const mappedConfig: { const mappedConfig: {
@@ -30,5 +31,12 @@ export function RaceHero(props: RaceHeroProps) {
variant: statusConfig.variant === 'default' ? 'default' : statusConfig.variant 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 { routes } from '@/lib/routing/RouteConfig';
import { RaceListItem as UiRaceListItem } from '@/components/races/RaceListItem'; 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 { interface Race {
id: string; id: string;
@@ -26,45 +27,32 @@ export function RaceListItem({ race, onClick }: RaceListItemProps) {
scheduled: { scheduled: {
iconName: 'Clock', iconName: 'Clock',
variant: 'primary' as const, variant: 'primary' as const,
label: 'Scheduled',
}, },
running: { running: {
iconName: 'PlayCircle', iconName: 'PlayCircle',
variant: 'success' as const, variant: 'success' as const,
label: 'LIVE',
}, },
completed: { completed: {
iconName: 'CheckCircle2', iconName: 'CheckCircle2',
variant: 'default' as const, variant: 'default' as const,
label: 'Completed',
}, },
cancelled: { cancelled: {
iconName: 'XCircle', iconName: 'XCircle',
variant: 'warning' as const, variant: 'warning' as const,
label: 'Cancelled',
}, },
}; };
const config = statusConfig[race.status]; 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 ( return (
<UiRaceListItem <UiRaceListItem
track={race.track} track={race.track}
car={race.car} car={race.car}
dateLabel={date.toLocaleDateString('en-US', { month: 'short' })} dateLabel={DateDisplay.formatMonthDay(race.scheduledAt).split(' ')[0]}
dayLabel={date.getDate().toString()} dayLabel={DateDisplay.formatMonthDay(race.scheduledAt).split(' ')[1]}
timeLabel={formatTime(race.scheduledAt)} timeLabel={DateDisplay.formatTime(race.scheduledAt)}
status={race.status} status={race.status}
statusLabel={config.label} statusLabel={StatusDisplay.raceStatus(race.status)}
statusVariant={config.variant} statusVariant={config.variant}
statusIconName={config.iconName} statusIconName={config.iconName}
leagueName={race.leagueName} leagueName={race.leagueName}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,9 +7,9 @@ import { AlertTriangle, Check, Clock, Download, Receipt } from 'lucide-react';
export interface Transaction { export interface Transaction {
id: string; id: string;
date: string; formattedDate: string;
description: string; description: string;
amount: number; formattedAmount: string;
status: 'paid' | 'pending' | 'overdue' | 'failed'; status: 'paid' | 'pending' | 'overdue' | 'failed';
invoiceNumber: string; invoiceNumber: string;
type: string; type: string;
@@ -103,11 +103,11 @@ export function TransactionTable({ transactions, onDownload }: TransactionTableP
</Stack> </Stack>
<Stack colSpan={{ base: 1, md: 2 } as any}> <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>
<Stack colSpan={{ base: 1, md: 2 } as any}> <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>
<Stack colSpan={{ base: 1, md: 2 } as any}> <Stack colSpan={{ base: 1, md: 2 } as any}>

View File

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

View File

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

View File

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

View File

@@ -9,10 +9,10 @@ interface TeamLeaderboardPanelProps {
id: string; id: string;
name: string; name: string;
logoUrl?: string; logoUrl?: string;
rating: number; ratingLabel: string;
wins: number; winsLabel: string;
races: number; racesLabel: string;
memberCount: number; memberCountLabel: string;
}>; }>;
onTeamClick: (id: string) => void; onTeamClick: (id: string) => void;
} }
@@ -53,16 +53,16 @@ export function TeamLeaderboardPanel({ teams, onTeamClick }: TeamLeaderboardPane
</Stack> </Stack>
</TableCell> </TableCell>
<TableCell className="text-center"> <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>
<TableCell className="text-center"> <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>
<TableCell className="text-center"> <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>
<TableCell className="text-center"> <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> </TableCell>
</TableRow> </TableRow>
))} ))}

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError'; import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
import { LeagueScheduleViewModel, LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueScheduleViewModel'; import { LeagueScheduleViewModel, LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueScheduleViewModel';
import type { RaceDTO } from '@/lib/types/generated/RaceDTO'; import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
function mapRaceDtoToViewModel(race: RaceDTO): LeagueScheduleRaceViewModel { function mapRaceDtoToViewModel(race: RaceDTO): LeagueScheduleRaceViewModel {
const scheduledAt = race.date ? new Date(race.date) : new Date(0); const scheduledAt = race.date ? new Date(race.date) : new Date(0);
@@ -14,6 +15,8 @@ function mapRaceDtoToViewModel(race: RaceDTO): LeagueScheduleRaceViewModel {
id: race.id, id: race.id,
name: race.name, name: race.name,
scheduledAt, scheduledAt,
formattedDate: DateDisplay.formatShort(scheduledAt),
formattedTime: DateDisplay.formatTime(scheduledAt),
isPast, isPast,
isUpcoming: !isPast, isUpcoming: !isPast,
status: isPast ? 'completed' : 'scheduled', status: isPast ? 'completed' : 'scheduled',

View File

@@ -7,6 +7,7 @@ import { LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueScheduleVie
import { LeagueSeasonSummaryViewModel } from '@/lib/view-models/LeagueSeasonSummaryViewModel'; import { LeagueSeasonSummaryViewModel } from '@/lib/view-models/LeagueSeasonSummaryViewModel';
import type { RaceDTO } from '@/lib/types/generated/RaceDTO'; import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
import type { LeagueSeasonSummaryDTO } from '@/lib/types/generated/LeagueSeasonSummaryDTO'; import type { LeagueSeasonSummaryDTO } from '@/lib/types/generated/LeagueSeasonSummaryDTO';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
function mapRaceDtoToViewModel(race: RaceDTO): LeagueScheduleRaceViewModel { function mapRaceDtoToViewModel(race: RaceDTO): LeagueScheduleRaceViewModel {
const scheduledAt = race.date ? new Date(race.date) : new Date(0); const scheduledAt = race.date ? new Date(race.date) : new Date(0);
@@ -17,6 +18,8 @@ function mapRaceDtoToViewModel(race: RaceDTO): LeagueScheduleRaceViewModel {
id: race.id, id: race.id,
name: race.name, name: race.name,
scheduledAt, scheduledAt,
formattedDate: DateDisplay.formatShort(scheduledAt),
formattedTime: DateDisplay.formatTime(scheduledAt),
isPast, isPast,
isUpcoming: !isPast, isUpcoming: !isPast,
status: isPast ? 'completed' : 'scheduled', status: isPast ? 'completed' : 'scheduled',

View File

@@ -1,5 +1,10 @@
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO'; import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
import type { DriverProfileViewData } from '@/lib/types/view-data/DriverProfileViewData'; import type { DriverProfileViewData } from '@/lib/types/view-data/DriverProfileViewData';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
import { FinishDisplay } from '@/lib/display-objects/FinishDisplay';
import { PercentDisplay } from '@/lib/display-objects/PercentDisplay';
/** /**
* DriverProfileViewDataBuilder * DriverProfileViewDataBuilder
@@ -17,26 +22,38 @@ export class DriverProfileViewDataBuilder {
avatarUrl: apiDto.currentDriver.avatarUrl || '', avatarUrl: apiDto.currentDriver.avatarUrl || '',
iracingId: typeof apiDto.currentDriver.iracingId === 'string' ? parseInt(apiDto.currentDriver.iracingId, 10) : (apiDto.currentDriver.iracingId ?? null), iracingId: typeof apiDto.currentDriver.iracingId === 'string' ? parseInt(apiDto.currentDriver.iracingId, 10) : (apiDto.currentDriver.iracingId ?? null),
joinedAt: apiDto.currentDriver.joinedAt, joinedAt: apiDto.currentDriver.joinedAt,
joinedAtLabel: DateDisplay.formatMonthYear(apiDto.currentDriver.joinedAt),
rating: apiDto.currentDriver.rating ?? null, rating: apiDto.currentDriver.rating ?? null,
ratingLabel: RatingDisplay.format(apiDto.currentDriver.rating),
globalRank: apiDto.currentDriver.globalRank ?? null, globalRank: apiDto.currentDriver.globalRank ?? null,
globalRankLabel: apiDto.currentDriver.globalRank != null ? `#${apiDto.currentDriver.globalRank}` : '—',
consistency: apiDto.currentDriver.consistency ?? null, consistency: apiDto.currentDriver.consistency ?? null,
bio: apiDto.currentDriver.bio ?? null, bio: apiDto.currentDriver.bio ?? null,
totalDrivers: apiDto.currentDriver.totalDrivers ?? null, totalDrivers: apiDto.currentDriver.totalDrivers ?? null,
} : null, } : null,
stats: apiDto.stats ? { stats: apiDto.stats ? {
totalRaces: apiDto.stats.totalRaces, totalRaces: apiDto.stats.totalRaces,
totalRacesLabel: NumberDisplay.format(apiDto.stats.totalRaces),
wins: apiDto.stats.wins, wins: apiDto.stats.wins,
winsLabel: NumberDisplay.format(apiDto.stats.wins),
podiums: apiDto.stats.podiums, podiums: apiDto.stats.podiums,
podiumsLabel: NumberDisplay.format(apiDto.stats.podiums),
dnfs: apiDto.stats.dnfs, dnfs: apiDto.stats.dnfs,
dnfsLabel: NumberDisplay.format(apiDto.stats.dnfs),
avgFinish: apiDto.stats.avgFinish ?? null, avgFinish: apiDto.stats.avgFinish ?? null,
avgFinishLabel: FinishDisplay.formatAverage(apiDto.stats.avgFinish),
bestFinish: apiDto.stats.bestFinish ?? null, bestFinish: apiDto.stats.bestFinish ?? null,
bestFinishLabel: FinishDisplay.format(apiDto.stats.bestFinish),
worstFinish: apiDto.stats.worstFinish ?? null, worstFinish: apiDto.stats.worstFinish ?? null,
worstFinishLabel: FinishDisplay.format(apiDto.stats.worstFinish),
finishRate: apiDto.stats.finishRate ?? null, finishRate: apiDto.stats.finishRate ?? null,
winRate: apiDto.stats.winRate ?? null, winRate: apiDto.stats.winRate ?? null,
podiumRate: apiDto.stats.podiumRate ?? null, podiumRate: apiDto.stats.podiumRate ?? null,
percentile: apiDto.stats.percentile ?? null, percentile: apiDto.stats.percentile ?? null,
rating: apiDto.stats.rating ?? null, rating: apiDto.stats.rating ?? null,
ratingLabel: RatingDisplay.format(apiDto.stats.rating),
consistency: apiDto.stats.consistency ?? null, consistency: apiDto.stats.consistency ?? null,
consistencyLabel: PercentDisplay.formatWhole(apiDto.stats.consistency),
overallRank: apiDto.stats.overallRank ?? null, overallRank: apiDto.stats.overallRank ?? null,
} : null, } : null,
finishDistribution: apiDto.finishDistribution ? { finishDistribution: apiDto.finishDistribution ? {
@@ -53,6 +70,7 @@ export class DriverProfileViewDataBuilder {
teamTag: m.teamTag ?? null, teamTag: m.teamTag ?? null,
role: m.role, role: m.role,
joinedAt: m.joinedAt, joinedAt: m.joinedAt,
joinedAtLabel: DateDisplay.formatMonthYear(m.joinedAt),
isCurrent: m.isCurrent, isCurrent: m.isCurrent,
})), })),
socialSummary: { socialSummary: {
@@ -76,7 +94,9 @@ export class DriverProfileViewDataBuilder {
description: a.description, description: a.description,
icon: a.icon, icon: a.icon,
rarity: a.rarity, rarity: a.rarity,
rarityLabel: a.rarity,
earnedAt: a.earnedAt, earnedAt: a.earnedAt,
earnedAtLabel: DateDisplay.formatShort(a.earnedAt),
})), })),
racingStyle: apiDto.extendedProfile.racingStyle, racingStyle: apiDto.extendedProfile.racingStyle,
favoriteTrack: apiDto.extendedProfile.favoriteTrack, favoriteTrack: apiDto.extendedProfile.favoriteTrack,
@@ -88,4 +108,4 @@ export class DriverProfileViewDataBuilder {
} : null, } : null,
}; };
} }
} }

View File

@@ -1,5 +1,7 @@
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO'; import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
import type { DriversViewData } from '@/lib/types/view-data/DriversViewData'; import type { DriversViewData } from '@/lib/types/view-data/DriversViewData';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
export class DriversViewDataBuilder { export class DriversViewDataBuilder {
static build(dto: DriversLeaderboardDTO): DriversViewData { static build(dto: DriversLeaderboardDTO): DriversViewData {
@@ -8,6 +10,7 @@ export class DriversViewDataBuilder {
id: driver.id, id: driver.id,
name: driver.name, name: driver.name,
rating: driver.rating, rating: driver.rating,
ratingLabel: RatingDisplay.format(driver.rating),
skillLevel: driver.skillLevel, skillLevel: driver.skillLevel,
category: driver.category, category: driver.category,
nationality: driver.nationality, nationality: driver.nationality,
@@ -19,8 +22,12 @@ export class DriversViewDataBuilder {
avatarUrl: driver.avatarUrl, avatarUrl: driver.avatarUrl,
})), })),
totalRaces: dto.totalRaces, totalRaces: dto.totalRaces,
totalRacesLabel: NumberDisplay.format(dto.totalRaces),
totalWins: dto.totalWins, totalWins: dto.totalWins,
totalWinsLabel: NumberDisplay.format(dto.totalWins),
activeCount: dto.activeCount, activeCount: dto.activeCount,
activeCountLabel: NumberDisplay.format(dto.activeCount),
totalDriversLabel: NumberDisplay.format(dto.drivers.length),
}; };
} }
} }

View File

@@ -1,6 +1,7 @@
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO'; import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO'; import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO';
import type { LeagueRosterAdminViewData, RosterMemberData, JoinRequestData } from '@/lib/view-data/LeagueRosterAdminViewData'; import type { LeagueRosterAdminViewData, RosterMemberData, JoinRequestData } from '@/lib/view-data/LeagueRosterAdminViewData';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
/** /**
* LeagueRosterAdminViewDataBuilder * LeagueRosterAdminViewDataBuilder
@@ -25,6 +26,7 @@ export class LeagueRosterAdminViewDataBuilder {
}, },
role: member.role, role: member.role,
joinedAt: member.joinedAt, joinedAt: member.joinedAt,
formattedJoinedAt: DateDisplay.formatShort(member.joinedAt),
})); }));
// Transform join requests // Transform join requests
@@ -35,6 +37,7 @@ export class LeagueRosterAdminViewDataBuilder {
name: 'Unknown Driver', // driver field is unknown type name: 'Unknown Driver', // driver field is unknown type
}, },
requestedAt: req.requestedAt, requestedAt: req.requestedAt,
formattedRequestedAt: DateDisplay.formatShort(req.requestedAt),
message: req.message, message: req.message,
})); }));

View File

@@ -1,5 +1,7 @@
import { LeagueSponsorshipsViewData } from '@/lib/view-data/leagues/LeagueSponsorshipsViewData'; import { LeagueSponsorshipsViewData } from '@/lib/view-data/leagues/LeagueSponsorshipsViewData';
import { LeagueSponsorshipsApiDto } from '@/lib/types/tbd/LeagueSponsorshipsApiDto'; import { LeagueSponsorshipsApiDto } from '@/lib/types/tbd/LeagueSponsorshipsApiDto';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import { StatusDisplay } from '@/lib/display-objects/StatusDisplay';
export class LeagueSponsorshipsViewDataBuilder { export class LeagueSponsorshipsViewDataBuilder {
static build(apiDto: LeagueSponsorshipsApiDto): LeagueSponsorshipsViewData { static build(apiDto: LeagueSponsorshipsApiDto): LeagueSponsorshipsViewData {
@@ -9,7 +11,11 @@ export class LeagueSponsorshipsViewDataBuilder {
onTabChange: () => {}, onTabChange: () => {},
league: apiDto.league, league: apiDto.league,
sponsorshipSlots: apiDto.sponsorshipSlots, sponsorshipSlots: apiDto.sponsorshipSlots,
sponsorshipRequests: apiDto.sponsorshipRequests, sponsorshipRequests: apiDto.sponsorshipRequests.map(r => ({
...r,
formattedRequestedAt: DateDisplay.formatShort(r.requestedAt),
statusLabel: StatusDisplay.protestStatus(r.status), // Reusing protest status for now
})),
}; };
} }
} }

View File

@@ -1,13 +1,15 @@
import { LeagueWalletViewData, LeagueWalletTransactionViewData } from '@/lib/view-data/leagues/LeagueWalletViewData'; import { LeagueWalletViewData, LeagueWalletTransactionViewData } from '@/lib/view-data/leagues/LeagueWalletViewData';
import { LeagueWalletApiDto } from '@/lib/types/tbd/LeagueWalletApiDto'; import { LeagueWalletApiDto } from '@/lib/types/tbd/LeagueWalletApiDto';
import { CurrencyDisplay } from '@/lib/display-objects/CurrencyDisplay';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
export class LeagueWalletViewDataBuilder { export class LeagueWalletViewDataBuilder {
static build(apiDto: LeagueWalletApiDto): LeagueWalletViewData { static build(apiDto: LeagueWalletApiDto): LeagueWalletViewData {
const transactions: LeagueWalletTransactionViewData[] = apiDto.transactions.map(t => ({ const transactions: LeagueWalletTransactionViewData[] = apiDto.transactions.map(t => ({
...t, ...t,
formattedAmount: `${t.amount} ${apiDto.currency}`, formattedAmount: CurrencyDisplay.format(t.amount, apiDto.currency),
amountColor: t.amount >= 0 ? 'green' : 'red', amountColor: t.amount >= 0 ? 'green' : 'red',
formattedDate: new Date(t.createdAt).toLocaleDateString(), formattedDate: DateDisplay.formatShort(t.createdAt),
statusColor: t.status === 'completed' ? 'green' : t.status === 'pending' ? 'yellow' : 'red', statusColor: t.status === 'completed' ? 'green' : t.status === 'pending' ? 'yellow' : 'red',
typeColor: 'blue', typeColor: 'blue',
})); }));
@@ -15,13 +17,13 @@ export class LeagueWalletViewDataBuilder {
return { return {
leagueId: apiDto.leagueId, leagueId: apiDto.leagueId,
balance: apiDto.balance, balance: apiDto.balance,
formattedBalance: `${apiDto.balance} ${apiDto.currency}`, formattedBalance: CurrencyDisplay.format(apiDto.balance, apiDto.currency),
totalRevenue: apiDto.balance, // Mock totalRevenue: apiDto.balance, // Mock
formattedTotalRevenue: `${apiDto.balance} ${apiDto.currency}`, formattedTotalRevenue: CurrencyDisplay.format(apiDto.balance, apiDto.currency),
totalFees: 0, // Mock totalFees: 0, // Mock
formattedTotalFees: `0 ${apiDto.currency}`, formattedTotalFees: CurrencyDisplay.format(0, apiDto.currency),
pendingPayouts: 0, // Mock pendingPayouts: 0, // Mock
formattedPendingPayouts: `0 ${apiDto.currency}`, formattedPendingPayouts: CurrencyDisplay.format(0, apiDto.currency),
currency: apiDto.currency, currency: apiDto.currency,
transactions, transactions,
}; };

View File

@@ -2,6 +2,11 @@ import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverP
import type { ProfileViewData } from '@/lib/view-data/ProfileViewData'; import type { ProfileViewData } from '@/lib/view-data/ProfileViewData';
import { mediaConfig } from '@/lib/config/mediaConfig'; import { mediaConfig } from '@/lib/config/mediaConfig';
import { CountryFlagDisplay } from '@/lib/display-objects/CountryFlagDisplay'; import { CountryFlagDisplay } from '@/lib/display-objects/CountryFlagDisplay';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import { FinishDisplay } from '@/lib/display-objects/FinishDisplay';
import { PercentDisplay } from '@/lib/display-objects/PercentDisplay';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
export class ProfileViewDataBuilder { export class ProfileViewDataBuilder {
static build(apiDto: GetDriverProfileOutputDTO): ProfileViewData { static build(apiDto: GetDriverProfileOutputDTO): ProfileViewData {
@@ -29,11 +34,6 @@ export class ProfileViewDataBuilder {
const socialSummary = apiDto.socialSummary; const socialSummary = apiDto.socialSummary;
const extended = apiDto.extendedProfile ?? null; const extended = apiDto.extendedProfile ?? null;
const joinedAtLabel = new Date(driver.joinedAt).toLocaleDateString('en-US', {
month: 'short',
year: 'numeric',
});
return { return {
driver: { driver: {
id: driver.id, id: driver.id,
@@ -42,22 +42,22 @@ export class ProfileViewDataBuilder {
countryFlag: CountryFlagDisplay.fromCountryCode(driver.country).toString(), countryFlag: CountryFlagDisplay.fromCountryCode(driver.country).toString(),
avatarUrl: driver.avatarUrl || mediaConfig.avatars.defaultFallback, avatarUrl: driver.avatarUrl || mediaConfig.avatars.defaultFallback,
bio: driver.bio || null, bio: driver.bio || null,
iracingId: driver.iracingId || null, iracingId: driver.iracingId ? String(driver.iracingId) : null,
joinedAtLabel, joinedAtLabel: DateDisplay.formatMonthYear(driver.joinedAt),
}, },
stats: stats stats: stats
? { ? {
ratingLabel: stats.rating != null ? String(stats.rating) : '0', ratingLabel: RatingDisplay.format(stats.rating),
globalRankLabel: driver.globalRank != null ? `#${driver.globalRank}` : '—', globalRankLabel: driver.globalRank != null ? `#${driver.globalRank}` : '—',
totalRacesLabel: String(stats.totalRaces), totalRacesLabel: NumberDisplay.format(stats.totalRaces),
winsLabel: String(stats.wins), winsLabel: NumberDisplay.format(stats.wins),
podiumsLabel: String(stats.podiums), podiumsLabel: NumberDisplay.format(stats.podiums),
dnfsLabel: String(stats.dnfs), dnfsLabel: NumberDisplay.format(stats.dnfs),
bestFinishLabel: stats.bestFinish != null ? `P${stats.bestFinish}` : '—', bestFinishLabel: FinishDisplay.format(stats.bestFinish),
worstFinishLabel: stats.worstFinish != null ? `P${stats.worstFinish}` : '—', worstFinishLabel: FinishDisplay.format(stats.worstFinish),
avgFinishLabel: stats.avgFinish != null ? `P${stats.avgFinish.toFixed(1)}` : '—', avgFinishLabel: FinishDisplay.formatAverage(stats.avgFinish),
consistencyLabel: stats.consistency != null ? `${stats.consistency}%` : '0%', consistencyLabel: PercentDisplay.formatWhole(stats.consistency),
percentileLabel: stats.percentile != null ? `${stats.percentile}%` : '—', percentileLabel: PercentDisplay.format(stats.percentile),
} }
: null, : null,
teamMemberships: apiDto.teamMemberships.map((m) => ({ teamMemberships: apiDto.teamMemberships.map((m) => ({
@@ -65,10 +65,7 @@ export class ProfileViewDataBuilder {
teamName: m.teamName, teamName: m.teamName,
teamTag: m.teamTag || null, teamTag: m.teamTag || null,
roleLabel: m.role, roleLabel: m.role,
joinedAtLabel: new Date(m.joinedAt).toLocaleDateString('en-US', { joinedAtLabel: DateDisplay.formatMonthYear(m.joinedAt),
month: 'short',
year: 'numeric',
}),
href: `/teams/${m.teamId}`, href: `/teams/${m.teamId}`,
})), })),
extendedProfile: extended extendedProfile: extended
@@ -89,12 +86,8 @@ export class ProfileViewDataBuilder {
id: a.id, id: a.id,
title: a.title, title: a.title,
description: a.description, description: a.description,
earnedAtLabel: new Date(a.earnedAt).toLocaleDateString('en-US', { earnedAtLabel: DateDisplay.formatShort(a.earnedAt),
month: 'short', icon: a.icon as any,
day: 'numeric',
year: 'numeric',
}),
icon: a.icon as NonNullable<ProfileViewData['extendedProfile']>['achievements'][number]['icon'],
rarityLabel: a.rarity, rarityLabel: a.rarity,
})), })),
friends: socialSummary.friends.slice(0, 8).map((f) => ({ friends: socialSummary.friends.slice(0, 8).map((f) => ({
@@ -104,7 +97,7 @@ export class ProfileViewDataBuilder {
avatarUrl: f.avatarUrl || mediaConfig.avatars.defaultFallback, avatarUrl: f.avatarUrl || mediaConfig.avatars.defaultFallback,
href: `/drivers/${f.id}`, href: `/drivers/${f.id}`,
})), })),
friendsCountLabel: String(socialSummary.friendsCount), friendsCountLabel: NumberDisplay.format(socialSummary.friendsCount),
} }
: null, : null,
}; };

View File

@@ -1,5 +1,7 @@
import type { SponsorDashboardDTO } from '@/lib/types/generated/SponsorDashboardDTO'; import type { SponsorDashboardDTO } from '@/lib/types/generated/SponsorDashboardDTO';
import type { SponsorDashboardViewData } from '@/lib/view-data/SponsorDashboardViewData'; import type { SponsorDashboardViewData } from '@/lib/view-data/SponsorDashboardViewData';
import { CurrencyDisplay } from '@/lib/display-objects/CurrencyDisplay';
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
/** /**
* Sponsor Dashboard ViewData Builder * Sponsor Dashboard ViewData Builder
@@ -9,26 +11,28 @@ import type { SponsorDashboardViewData } from '@/lib/view-data/SponsorDashboardV
*/ */
export class SponsorDashboardViewDataBuilder { export class SponsorDashboardViewDataBuilder {
static build(apiDto: SponsorDashboardDTO): SponsorDashboardViewData { static build(apiDto: SponsorDashboardDTO): SponsorDashboardViewData {
const totalInvestmentValue = apiDto.investment.activeSponsorships * 1000;
return { return {
sponsorName: apiDto.sponsorName, sponsorName: apiDto.sponsorName,
totalImpressions: apiDto.metrics.impressions.toString(), totalImpressions: NumberDisplay.format(apiDto.metrics.impressions),
totalInvestment: `$${apiDto.investment.activeSponsorships * 1000}`, // Mock calculation totalInvestment: CurrencyDisplay.format(totalInvestmentValue),
metrics: { metrics: {
impressionsChange: apiDto.metrics.impressions > 1000 ? 15 : -5, impressionsChange: apiDto.metrics.impressions > 1000 ? 15 : -5,
viewersChange: 8, viewersChange: 8,
exposureChange: 12, exposureChange: 12,
}, },
categoryData: { categoryData: {
leagues: { count: 2, impressions: 1500 }, leagues: { count: 2, countLabel: '2 active', impressions: 1500, impressionsLabel: '1,500' },
teams: { count: 1, impressions: 800 }, teams: { count: 1, countLabel: '1 active', impressions: 800, impressionsLabel: '800' },
drivers: { count: 3, impressions: 2200 }, drivers: { count: 3, countLabel: '3 active', impressions: 2200, impressionsLabel: '2,200' },
races: { count: 1, impressions: 500 }, races: { count: 1, countLabel: '1 active', impressions: 500, impressionsLabel: '500' },
platform: { count: 0, impressions: 0 }, platform: { count: 0, countLabel: '0 active', impressions: 0, impressionsLabel: '0' },
}, },
sponsorships: apiDto.sponsorships, sponsorships: apiDto.sponsorships,
activeSponsorships: apiDto.investment.activeSponsorships, activeSponsorships: apiDto.investment.activeSponsorships,
formattedTotalInvestment: `$${apiDto.investment.activeSponsorships * 1000}`, formattedTotalInvestment: CurrencyDisplay.format(totalInvestmentValue),
costPerThousandViews: '$50', costPerThousandViews: CurrencyDisplay.format(50),
upcomingRenewals: [], // Mock empty for now upcomingRenewals: [], // Mock empty for now
recentActivity: [], // Mock empty for now recentActivity: [], // Mock empty for now
}; };

View File

@@ -3,6 +3,7 @@ import type { TeamDetailViewData, TeamDetailData, TeamMemberData, SponsorMetric,
import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import { MemberDisplay } from '@/lib/display-objects/MemberDisplay'; import { MemberDisplay } from '@/lib/display-objects/MemberDisplay';
import { LeagueDisplay } from '@/lib/display-objects/LeagueDisplay'; import { LeagueDisplay } from '@/lib/display-objects/LeagueDisplay';
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
/** /**
* TeamDetailViewDataBuilder - Transforms TeamDetailPageDto into ViewData * TeamDetailViewDataBuilder - Transforms TeamDetailPageDto into ViewData
@@ -47,19 +48,19 @@ export class TeamDetailViewDataBuilder {
{ {
icon: 'users', icon: 'users',
label: 'Members', label: 'Members',
value: memberships.length, value: NumberDisplay.format(memberships.length),
color: 'text-primary-blue', color: 'text-primary-blue',
}, },
{ {
icon: 'zap', icon: 'zap',
label: 'Est. Reach', label: 'Est. Reach',
value: memberships.length * 15, value: NumberDisplay.format(memberships.length * 15),
color: 'text-purple-400', color: 'text-purple-400',
}, },
{ {
icon: 'calendar', icon: 'calendar',
label: 'Races', label: 'Races',
value: leagueCount, value: NumberDisplay.format(leagueCount),
color: 'text-neon-aqua', color: 'text-neon-aqua',
}, },
{ {

View File

@@ -1,6 +1,8 @@
import type { TeamsPageDto } from '@/lib/page-queries/TeamsPageQuery'; import type { TeamsPageDto } from '@/lib/page-queries/TeamsPageQuery';
import type { TeamsViewData, TeamSummaryData } from '@/lib/view-data/TeamsViewData'; import type { TeamsViewData, TeamSummaryData } from '@/lib/view-data/TeamsViewData';
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO'; import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
/** /**
* TeamsViewDataBuilder - Transforms TeamsPageDto into ViewData for TeamsTemplate * TeamsViewDataBuilder - Transforms TeamsPageDto into ViewData for TeamsTemplate
@@ -14,6 +16,9 @@ export class TeamsViewDataBuilder {
leagueName: team.leagues[0] || '', leagueName: team.leagues[0] || '',
memberCount: team.memberCount, memberCount: team.memberCount,
logoUrl: team.logoUrl, logoUrl: team.logoUrl,
ratingLabel: RatingDisplay.format(team.rating),
winsLabel: NumberDisplay.format(team.totalWins || 0),
racesLabel: NumberDisplay.format(team.totalRaces || 0),
})); }));
return { teams }; return { teams };

View File

@@ -0,0 +1,24 @@
/**
* DurationDisplay
*
* Deterministic formatting for time durations.
*/
export class DurationDisplay {
/**
* Formats milliseconds as "123.45ms".
*/
static formatMs(ms: number): string {
return `${ms.toFixed(2)}ms`;
}
/**
* Formats seconds as "M:SS.mmm".
* Example: 65.123 -> "1:05.123"
*/
static formatSeconds(seconds: number): string {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = (seconds % 60).toFixed(3);
return `${minutes}:${remainingSeconds.padStart(6, '0')}`;
}
}

View File

@@ -0,0 +1,24 @@
/**
* FinishDisplay
*
* Deterministic formatting for race finish positions.
*/
export class FinishDisplay {
/**
* Formats a finish position as "P1", "P2", etc.
*/
static format(position: number | null | undefined): string {
if (position === null || position === undefined) return '—';
return `P${position.toFixed(0)}`;
}
/**
* Formats an average finish position with one decimal place.
* Example: 5.4 -> "P5.4"
*/
static formatAverage(avg: number | null | undefined): string {
if (avg === null || avg === undefined) return '—';
return `P${avg.toFixed(1)}`;
}
}

View File

@@ -0,0 +1,21 @@
/**
* MemoryDisplay
*
* Deterministic formatting for memory usage.
*/
export class MemoryDisplay {
/**
* Formats bytes as "123.4MB".
*/
static formatMB(bytes: number): string {
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
}
/**
* Formats bytes as "123.4KB".
*/
static formatKB(bytes: number): string {
return `${(bytes / 1024).toFixed(1)}KB`;
}
}

View File

@@ -15,4 +15,17 @@ export class NumberDisplay {
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return parts.join('.'); return parts.join('.');
} }
/**
* Formats a number in compact form (e.g., 1.2k, 1.5M).
*/
static formatCompact(value: number): string {
if (value >= 1000000) {
return `${(value / 1000000).toFixed(1)}M`;
}
if (value >= 1000) {
return `${(value / 1000).toFixed(1)}k`;
}
return value.toString();
}
} }

View File

@@ -0,0 +1,25 @@
/**
* PercentDisplay
*
* Deterministic formatting for percentages.
*/
export class PercentDisplay {
/**
* Formats a decimal value as a percentage string.
* Example: 0.1234 -> "12.3%"
*/
static format(value: number | null | undefined): string {
if (value === null || value === undefined) return '0.0%';
return `${(value * 100).toFixed(1)}%`;
}
/**
* Formats a whole number as a percentage string.
* Example: 85 -> "85%"
*/
static formatWhole(value: number | null | undefined): string {
if (value === null || value === undefined) return '0%';
return `${Math.round(value)}%`;
}
}

View File

@@ -0,0 +1,44 @@
/**
* StatusDisplay
*
* Deterministic mapping of status codes to human-readable labels.
*/
export class StatusDisplay {
/**
* Maps transaction status to label.
*/
static transactionStatus(status: string): string {
const map: Record<string, string> = {
paid: 'Paid',
pending: 'Pending',
overdue: 'Overdue',
failed: 'Failed',
};
return map[status] || status;
}
/**
* Maps race status to label.
*/
static raceStatus(status: string): string {
const map: Record<string, string> = {
scheduled: 'Scheduled',
running: 'Live',
completed: 'Completed',
};
return map[status] || status;
}
/**
* Maps protest status to label.
*/
static protestStatus(status: string): string {
const map: Record<string, string> = {
pending: 'Pending',
under_review: 'Under Review',
resolved: 'Resolved',
};
return map[status] || status;
}
}

View File

@@ -4,4 +4,9 @@ export class WinRateDisplay {
const rate = (wins / racesCompleted) * 100; const rate = (wins / racesCompleted) * 100;
return rate.toFixed(1); return rate.toFixed(1);
} }
static format(rate: number | null | undefined): string {
if (rate === null || rate === undefined) return '0.0%';
return `${rate.toFixed(1)}%`;
}
} }

View File

@@ -18,6 +18,8 @@ import { Result } from '@/lib/contracts/Result';
import type { Service } from '@/lib/contracts/services/Service'; import type { Service } from '@/lib/contracts/services/Service';
import type { HomeDataDTO } from '@/lib/types/dtos/HomeDataDTO'; import type { HomeDataDTO } from '@/lib/types/dtos/HomeDataDTO';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
/** /**
* HomeService * HomeService
* *
@@ -54,7 +56,7 @@ export class HomeService implements Service {
id: r.id, id: r.id,
track: r.track, track: r.track,
car: r.car, car: r.car,
formattedDate: new Date(r.scheduledAt).toLocaleDateString(), formattedDate: DateDisplay.formatShort(r.scheduledAt),
})), })),
topLeagues: leaguesDto.leagues.slice(0, 4).map(l => ({ topLeagues: leaguesDto.leagues.slice(0, 4).map(l => ({
id: l.id, id: l.id,

View File

@@ -6,26 +6,38 @@ export interface DriverProfileViewData {
avatarUrl: string; avatarUrl: string;
iracingId: number | null; iracingId: number | null;
joinedAt: string; joinedAt: string;
joinedAtLabel: string;
rating: number | null; rating: number | null;
ratingLabel: string;
globalRank: number | null; globalRank: number | null;
globalRankLabel: string;
consistency: number | null; consistency: number | null;
bio: string | null; bio: string | null;
totalDrivers: number | null; totalDrivers: number | null;
} | null; } | null;
stats: { stats: {
totalRaces: number; totalRaces: number;
totalRacesLabel: string;
wins: number; wins: number;
winsLabel: string;
podiums: number; podiums: number;
podiumsLabel: string;
dnfs: number; dnfs: number;
dnfsLabel: string;
avgFinish: number | null; avgFinish: number | null;
avgFinishLabel: string;
bestFinish: number | null; bestFinish: number | null;
bestFinishLabel: string;
worstFinish: number | null; worstFinish: number | null;
worstFinishLabel: string;
finishRate: number | null; finishRate: number | null;
winRate: number | null; winRate: number | null;
podiumRate: number | null; podiumRate: number | null;
percentile: number | null; percentile: number | null;
rating: number | null; rating: number | null;
ratingLabel: string;
consistency: number | null; consistency: number | null;
consistencyLabel: string;
overallRank: number | null; overallRank: number | null;
} | null; } | null;
finishDistribution: { finishDistribution: {
@@ -42,6 +54,7 @@ export interface DriverProfileViewData {
teamTag: string | null; teamTag: string | null;
role: string; role: string;
joinedAt: string; joinedAt: string;
joinedAtLabel: string;
isCurrent: boolean; isCurrent: boolean;
}[]; }[];
socialSummary: { socialSummary: {
@@ -65,7 +78,9 @@ export interface DriverProfileViewData {
description: string; description: string;
icon: string; icon: string;
rarity: string; rarity: string;
rarityLabel: string;
earnedAt: string; earnedAt: string;
earnedAtLabel: string;
}[]; }[];
racingStyle: string; racingStyle: string;
favoriteTrack: string; favoriteTrack: string;

View File

@@ -3,6 +3,7 @@ export interface DriversViewData {
id: string; id: string;
name: string; name: string;
rating: number; rating: number;
ratingLabel: string;
skillLevel: string; skillLevel: string;
category?: string; category?: string;
nationality: string; nationality: string;
@@ -14,6 +15,10 @@ export interface DriversViewData {
avatarUrl?: string; avatarUrl?: string;
}[]; }[];
totalRaces: number; totalRaces: number;
totalRacesLabel: string;
totalWins: number; totalWins: number;
totalWinsLabel: string;
activeCount: number; activeCount: number;
activeCountLabel: string;
totalDriversLabel: string;
} }

View File

@@ -11,6 +11,7 @@ export interface RosterMemberData {
}; };
role: string; role: string;
joinedAt: string; joinedAt: string;
formattedJoinedAt: string;
} }
export interface JoinRequestData { export interface JoinRequestData {
@@ -20,6 +21,7 @@ export interface JoinRequestData {
name: string; name: string;
}; };
requestedAt: string; requestedAt: string;
formattedRequestedAt: string;
message?: string; message?: string;
} }

View File

@@ -8,11 +8,11 @@ export interface SponsorDashboardViewData {
exposureChange: number; exposureChange: number;
}; };
categoryData: { categoryData: {
leagues: { count: number; impressions: number }; leagues: { count: number; countLabel: string; impressions: number; impressionsLabel: string };
teams: { count: number; impressions: number }; teams: { count: number; countLabel: string; impressions: number; impressionsLabel: string };
drivers: { count: number; impressions: number }; drivers: { count: number; countLabel: string; impressions: number; impressionsLabel: string };
races: { count: number; impressions: number }; races: { count: number; countLabel: string; impressions: number; impressionsLabel: string };
platform: { count: number; impressions: number }; platform: { count: number; countLabel: string; impressions: number; impressionsLabel: string };
}; };
sponsorships: Record<string, unknown>; // From DTO sponsorships: Record<string, unknown>; // From DTO
activeSponsorships: number; activeSponsorships: number;

View File

@@ -6,10 +6,10 @@
export interface SponsorMetric { export interface SponsorMetric {
icon: string; // Icon name (e.g. 'users', 'zap', 'calendar') icon: string; // Icon name (e.g. 'users', 'zap', 'calendar')
label: string; label: string;
value: string | number; value: string;
color?: string; color?: string;
trend?: { trend?: {
value: number; value: string;
isPositive: boolean; isPositive: boolean;
}; };
} }

View File

@@ -11,6 +11,9 @@ export interface TeamSummaryData {
leagueName: string; leagueName: string;
memberCount: number; memberCount: number;
logoUrl?: string; logoUrl?: string;
ratingLabel: string;
winsLabel: string;
racesLabel: string;
} }
export interface TeamsViewData extends ViewData { export interface TeamsViewData extends ViewData {

View File

@@ -26,6 +26,8 @@ export interface LeagueSponsorshipsViewData {
sponsorId: string; sponsorId: string;
sponsorName: string; sponsorName: string;
requestedAt: string; requestedAt: string;
formattedRequestedAt: string;
status: 'pending' | 'approved' | 'rejected'; status: 'pending' | 'approved' | 'rejected';
statusLabel: string;
}>; }>;
} }

View File

@@ -7,6 +7,8 @@ export interface LeagueScheduleRaceViewModel {
id: string; id: string;
name: string; name: string;
scheduledAt: Date; scheduledAt: Date;
formattedDate: string;
formattedTime: string;
isPast: boolean; isPast: boolean;
isUpcoming: boolean; isUpcoming: boolean;
status: string; status: string;

View File

@@ -1,5 +1,7 @@
import { ProtestDTO } from '@/lib/types/generated/ProtestDTO'; import { ProtestDTO } from '@/lib/types/generated/ProtestDTO';
import { RaceProtestDTO } from '@/lib/types/generated/RaceProtestDTO'; import { RaceProtestDTO } from '@/lib/types/generated/RaceProtestDTO';
import { DateDisplay } from '../display-objects/DateDisplay';
import { StatusDisplay } from '../display-objects/StatusDisplay';
/** /**
* Protest view model * Protest view model
@@ -96,11 +98,11 @@ export class ProtestViewModel {
/** UI-specific: Formatted submitted date */ /** UI-specific: Formatted submitted date */
get formattedSubmittedAt(): string { get formattedSubmittedAt(): string {
return new Date(this.submittedAt).toLocaleString(); return DateDisplay.formatShort(this.submittedAt);
} }
/** UI-specific: Status display */ /** UI-specific: Status display */
get statusDisplay(): string { get statusDisplay(): string {
return 'Pending'; return StatusDisplay.protestStatus(this.status);
} }
} }

View File

@@ -1,4 +1,5 @@
import { RaceResultDTO } from '@/lib/types/generated/RaceResultDTO'; import { RaceResultDTO } from '@/lib/types/generated/RaceResultDTO';
import { FinishDisplay } from '../display-objects/FinishDisplay';
export class RaceResultViewModel { export class RaceResultViewModel {
driverId!: string; driverId!: string;
@@ -42,7 +43,7 @@ export class RaceResultViewModel {
/** UI-specific: Badge for position */ /** UI-specific: Badge for position */
get positionBadge(): string { get positionBadge(): string {
return this.position.toString(); return FinishDisplay.format(this.position);
} }
/** UI-specific: Color for incidents badge */ /** UI-specific: Color for incidents badge */
@@ -66,6 +67,25 @@ export class RaceResultViewModel {
return this.positionChange; return this.positionChange;
} }
get formattedPosition(): string {
return FinishDisplay.format(this.position);
}
get formattedStartPosition(): string {
return FinishDisplay.format(this.startPosition);
}
get formattedIncidents(): string {
return `${this.incidents}x incidents`;
}
get formattedPositionsGained(): string | undefined {
if (this.position < this.startPosition) {
return `+${this.startPosition - this.position} positions`;
}
return undefined;
}
// Note: The generated DTO doesn't have id or raceId // Note: The generated DTO doesn't have id or raceId
// These will need to be added when the OpenAPI spec is updated // These will need to be added when the OpenAPI spec is updated
id: string = ''; id: string = '';

View File

@@ -1,3 +1,7 @@
import { CurrencyDisplay } from '../display-objects/CurrencyDisplay';
import { DateDisplay } from '../display-objects/DateDisplay';
import { NumberDisplay } from '../display-objects/NumberDisplay';
/** /**
* Interface for sponsorship data input * Interface for sponsorship data input
*/ */
@@ -69,11 +73,11 @@ export class SponsorshipViewModel {
} }
get formattedImpressions(): string { get formattedImpressions(): string {
return this.impressions.toLocaleString(); return NumberDisplay.format(this.impressions);
} }
get formattedPrice(): string { get formattedPrice(): string {
return `$${this.price}`; return CurrencyDisplay.format(this.price);
} }
get daysRemaining(): number { get daysRemaining(): number {
@@ -109,8 +113,8 @@ export class SponsorshipViewModel {
} }
get periodDisplay(): string { get periodDisplay(): string {
const start = this.startDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }); const start = DateDisplay.formatMonthYear(this.startDate);
const end = this.endDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }); const end = DateDisplay.formatMonthYear(this.endDate);
return `${start} - ${end}`; return `${start} - ${end}`;
} }
} }

View File

@@ -75,12 +75,12 @@ export function DriverProfileTemplate({
const { currentDriver, stats, teamMemberships, socialSummary, extendedProfile } = viewData; const { currentDriver, stats, teamMemberships, socialSummary, extendedProfile } = viewData;
const careerStats = stats ? [ const careerStats = stats ? [
{ label: 'Rating', value: stats.rating || 0, color: 'text-primary-blue' }, { label: 'Rating', value: stats.ratingLabel, color: 'text-primary-blue' },
{ label: 'Wins', value: stats.wins, color: 'text-performance-green' }, { label: 'Wins', value: stats.winsLabel, color: 'text-performance-green' },
{ label: 'Podiums', value: stats.podiums, color: 'text-warning-amber' }, { label: 'Podiums', value: stats.podiumsLabel, color: 'text-warning-amber' },
{ label: 'Total Races', value: stats.totalRaces }, { label: 'Total Races', value: stats.totalRacesLabel },
{ label: 'Avg Finish', value: stats.avgFinish?.toFixed(1) || '-', subValue: 'POS' }, { label: 'Avg Finish', value: stats.avgFinishLabel, subValue: 'POS' },
{ label: 'Consistency', value: stats.consistency ? `${stats.consistency}%` : '-' }, { label: 'Consistency', value: stats.consistencyLabel, color: 'text-primary-blue' },
] : []; ] : [];
return ( return (
@@ -115,7 +115,9 @@ export function DriverProfileTemplate({
avatarUrl={currentDriver.avatarUrl} avatarUrl={currentDriver.avatarUrl}
nationality={currentDriver.country} nationality={currentDriver.country}
rating={stats?.rating || 0} rating={stats?.rating || 0}
globalRank={currentDriver.globalRank ?? undefined} ratingLabel={currentDriver.ratingLabel}
safetyRatingLabel="SR 92"
globalRankLabel={currentDriver.globalRankLabel}
bio={currentDriver.bio} bio={currentDriver.bio}
friendRequestSent={friendRequestSent} friendRequestSent={friendRequestSent}
onAddFriend={onAddFriend} onAddFriend={onAddFriend}
@@ -132,7 +134,7 @@ export function DriverProfileTemplate({
memberships={teamMemberships.map((m) => ({ memberships={teamMemberships.map((m) => ({
team: { id: m.teamId, name: m.teamName }, team: { id: m.teamId, name: m.teamName },
role: m.role, role: m.role,
joinedAt: new Date(m.joinedAt) joinedAtLabel: m.joinedAtLabel
}))} }))}
/> />
)} )}
@@ -172,7 +174,8 @@ export function DriverProfileTemplate({
<AchievementGrid <AchievementGrid
achievements={extendedProfile.achievements.map((a) => ({ achievements={extendedProfile.achievements.map((a) => ({
...a, ...a,
earnedAt: new Date(a.earnedAt) rarity: a.rarityLabel,
earnedAtLabel: a.earnedAtLabel
}))} }))}
/> />
)} )}

View File

@@ -36,10 +36,10 @@ export function DriversTemplate({
<Container size="lg" py={8}> <Container size="lg" py={8}>
<Stack gap={10}> <Stack gap={10}>
<DriversDirectoryHeader <DriversDirectoryHeader
totalDrivers={drivers.length} totalDriversLabel={viewData?.totalDriversLabel || '0'}
activeDrivers={activeCount} activeDriversLabel={viewData?.activeCountLabel || '0'}
totalWins={totalWins} totalWinsLabel={viewData?.totalWinsLabel || '0'}
totalRaces={totalRaces} totalRacesLabel={viewData?.totalRacesLabel || '0'}
onViewLeaderboard={onViewLeaderboard} onViewLeaderboard={onViewLeaderboard}
/> />
@@ -54,7 +54,8 @@ export function DriversTemplate({
avatarUrl={driver.avatarUrl} avatarUrl={driver.avatarUrl}
nationality={driver.nationality} nationality={driver.nationality}
rating={driver.rating} rating={driver.rating}
wins={driver.wins} ratingLabel={driver.ratingLabel}
winsLabel={String(driver.wins)}
onClick={() => onDriverClick(driver.id)} onClick={() => onDriverClick(driver.id)}
/> />
))} ))}

View File

@@ -121,7 +121,7 @@ export function LeagueSponsorshipsTemplate({ viewData }: LeagueSponsorshipsTempl
key={request.id} key={request.id}
request={{ request={{
...request, ...request,
slotName: slot?.name || 'Unknown slot' slotName: slot?.name || 'Unknown slot',
}} }}
/> />
); );

View File

@@ -40,9 +40,9 @@ export function LeagueWalletTemplate({ viewData, onExport, transactions }: Leagu
</SharedBox> </SharedBox>
<WalletSummaryPanel <WalletSummaryPanel
balance={viewData.balance} formattedBalance={viewData.formattedBalance}
currency="USD" currency="USD"
transactions={transactions} transactions={viewData.transactions}
onDeposit={() => {}} // Not implemented for leagues yet onDeposit={() => {}} // Not implemented for leagues yet
onWithdraw={() => {}} // Not implemented for leagues yet onWithdraw={() => {}} // Not implemented for leagues yet
/> />

View File

@@ -70,10 +70,9 @@ export function ProfileTemplate({
...viewData.driver, ...viewData.driver,
country: viewData.driver.countryCode, country: viewData.driver.countryCode,
iracingId: Number(viewData.driver.iracingId) || 0, iracingId: Number(viewData.driver.iracingId) || 0,
joinedAt: new Date().toISOString(), // Placeholder
}} }}
stats={viewData.stats ? { rating: Number(viewData.stats.ratingLabel) || 0 } : null} stats={viewData.stats ? { ratingLabel: viewData.stats.ratingLabel } : null}
globalRank={Number(viewData.stats?.globalRankLabel) || 0} globalRankLabel={viewData.stats?.globalRankLabel || '—'}
onAddFriend={onFriendRequestSend} onAddFriend={onFriendRequestSend}
friendRequestSent={friendRequestSent} friendRequestSent={friendRequestSent}
isOwnProfile={true} isOwnProfile={true}
@@ -99,7 +98,7 @@ export function ProfileTemplate({
memberships={viewData.teamMemberships.map(m => ({ memberships={viewData.teamMemberships.map(m => ({
team: { id: m.teamId, name: m.teamName }, team: { id: m.teamId, name: m.teamName },
role: m.roleLabel, role: m.roleLabel,
joinedAt: new Date() // Placeholder joinedAtLabel: m.joinedAtLabel
}))} }))}
/> />
</Stack> </Stack>
@@ -117,7 +116,7 @@ export function ProfileTemplate({
achievements={viewData.extendedProfile.achievements.map(a => ({ achievements={viewData.extendedProfile.achievements.map(a => ({
...a, ...a,
rarity: a.rarityLabel, rarity: a.rarityLabel,
earnedAt: new Date() // Placeholder earnedAtLabel: a.earnedAtLabel
}))} }))}
/> />
</Stack> </Stack>

View File

@@ -62,7 +62,7 @@ export function RosterAdminTemplate({
<Stack direction={{ base: 'col', md: 'row' }} align="center" justify="between" gap={4}> <Stack direction={{ base: 'col', md: 'row' }} align="center" justify="between" gap={4}>
<Stack gap={1}> <Stack gap={1}>
<Text weight="bold" color="text-white">{req.driver.name}</Text> <Text weight="bold" color="text-white">{req.driver.name}</Text>
<Text size="xs" color="text-gray-500">{new Date(req.requestedAt).toLocaleString()}</Text> <Text size="xs" color="text-gray-500">{req.formattedRequestedAt}</Text>
{req.message && ( {req.message && (
<Text size="sm" color="text-gray-400" mt={1}>&quot;{req.message}&quot;</Text> <Text size="sm" color="text-gray-400" mt={1}>&quot;{req.message}&quot;</Text>
)} )}
@@ -117,7 +117,7 @@ export function RosterAdminTemplate({
<Text weight="bold" color="text-white">{member.driver.name}</Text> <Text weight="bold" color="text-white">{member.driver.name}</Text>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Text size="sm" color="text-gray-400">{new Date(member.joinedAt).toLocaleDateString()}</Text> <Text size="sm" color="text-gray-400">{member.formattedJoinedAt}</Text>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Box <Box

View File

@@ -27,10 +27,10 @@ export interface SponsorCampaignsViewData {
leagueName: string; leagueName: string;
seasonName: string; seasonName: string;
tier: string; tier: string;
pricing: { amount: number; currency: string }; formattedInvestment: string;
metrics: { impressions: number }; formattedImpressions: string;
seasonStartDate?: Date; formattedStartDate?: string;
seasonEndDate?: Date; formattedEndDate?: string;
}>; }>;
stats: { stats: {
total: number; total: number;
@@ -38,8 +38,8 @@ export interface SponsorCampaignsViewData {
pending: number; pending: number;
approved: number; approved: number;
rejected: number; rejected: number;
totalInvestment: number; formattedTotalInvestment: string;
totalImpressions: number; formattedTotalImpressions: string;
}; };
} }
@@ -80,13 +80,13 @@ export function SponsorCampaignsTemplate({
}, },
{ {
label: 'Total Investment', label: 'Total Investment',
value: `$${viewData.stats.totalInvestment.toLocaleString()}`, value: viewData.stats.formattedTotalInvestment,
icon: BarChart3, icon: BarChart3,
variant: 'info', variant: 'info',
}, },
{ {
label: 'Total Impressions', label: 'Total Impressions',
value: `${(viewData.stats.totalImpressions / 1000).toFixed(0)}k`, value: viewData.stats.formattedTotalImpressions,
icon: Eye, icon: Eye,
variant: 'default', variant: 'default',
}, },
@@ -153,10 +153,10 @@ export function SponsorCampaignsTemplate({
title={s.leagueName} title={s.leagueName}
subtitle={s.seasonName} subtitle={s.seasonName}
tier={s.tier} tier={s.tier}
investment={`$${s.pricing.amount.toLocaleString()}`} investment={s.formattedInvestment}
impressions={s.metrics.impressions.toLocaleString()} impressions={s.formattedImpressions}
startDate={s.seasonStartDate ? new Date(s.seasonStartDate).toLocaleDateString() : undefined} startDate={s.formattedStartDate}
endDate={s.seasonEndDate ? new Date(s.seasonEndDate).toLocaleDateString() : undefined} endDate={s.formattedEndDate}
/> />
); );
})} })}

View File

@@ -148,40 +148,40 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
<SponsorshipCategoryCard <SponsorshipCategoryCard
icon={Trophy} icon={Trophy}
title="Leagues" title="Leagues"
count={categoryData.leagues.count} countLabel={categoryData.leagues.countLabel}
impressions={categoryData.leagues.impressions} impressionsLabel={categoryData.leagues.impressionsLabel}
color="#3b82f6" color="#3b82f6"
href="/sponsor/campaigns?type=leagues" href="/sponsor/campaigns?type=leagues"
/> />
<SponsorshipCategoryCard <SponsorshipCategoryCard
icon={Users} icon={Users}
title="Teams" title="Teams"
count={categoryData.teams.count} countLabel={categoryData.teams.countLabel}
impressions={categoryData.teams.impressions} impressionsLabel={categoryData.teams.impressionsLabel}
color="#a855f7" color="#a855f7"
href="/sponsor/campaigns?type=teams" href="/sponsor/campaigns?type=teams"
/> />
<SponsorshipCategoryCard <SponsorshipCategoryCard
icon={Car} icon={Car}
title="Drivers" title="Drivers"
count={categoryData.drivers.count} countLabel={categoryData.drivers.countLabel}
impressions={categoryData.drivers.impressions} impressionsLabel={categoryData.drivers.impressionsLabel}
color="#10b981" color="#10b981"
href="/sponsor/campaigns?type=drivers" href="/sponsor/campaigns?type=drivers"
/> />
<SponsorshipCategoryCard <SponsorshipCategoryCard
icon={Flag} icon={Flag}
title="Races" title="Races"
count={categoryData.races.count} countLabel={categoryData.races.countLabel}
impressions={categoryData.races.impressions} impressionsLabel={categoryData.races.impressionsLabel}
color="#f59e0b" color="#f59e0b"
href="/sponsor/campaigns?type=races" href="/sponsor/campaigns?type=races"
/> />
<SponsorshipCategoryCard <SponsorshipCategoryCard
icon={Megaphone} icon={Megaphone}
title="Platform Ads" title="Platform Ads"
count={categoryData.platform.count} countLabel={categoryData.platform.countLabel}
impressions={categoryData.platform.impressions} impressionsLabel={categoryData.platform.impressionsLabel}
color="#ef4444" color="#ef4444"
href="/sponsor/campaigns?type=platform" href="/sponsor/campaigns?type=platform"
/> />

View File

@@ -45,11 +45,16 @@ export interface SponsorLeagueDetailViewData extends ViewData {
description: string; description: string;
tier: 'premium' | 'standard' | 'starter'; tier: 'premium' | 'standard' | 'starter';
rating: number; rating: number;
formattedRating: string;
drivers: number; drivers: number;
formattedDrivers: string;
races: number; races: number;
formattedRaces: string;
completedRaces: number; completedRaces: number;
racesLeft: number; racesLeft: number;
formattedRacesLeft: string;
engagement: number; engagement: number;
formattedEngagement: string;
totalImpressions: number; totalImpressions: number;
formattedTotalImpressions: string; formattedTotalImpressions: string;
projectedTotal: number; projectedTotal: number;
@@ -61,17 +66,20 @@ export interface SponsorLeagueDetailViewData extends ViewData {
nextRace?: { nextRace?: {
name: string; name: string;
date: string; date: string;
formattedDate: string;
}; };
sponsorSlots: { sponsorSlots: {
main: { main: {
available: boolean; available: boolean;
price: number; price: number;
priceLabel: string;
benefits: string[]; benefits: string[];
}; };
secondary: { secondary: {
available: number; available: number;
total: number; total: number;
price: number; price: number;
priceLabel: string;
benefits: string[]; benefits: string[];
}; };
}; };
@@ -84,10 +92,12 @@ export interface SponsorLeagueDetailViewData extends ViewData {
drivers: Array<{ drivers: Array<{
id: string; id: string;
position: number; position: number;
positionLabel: string;
name: string; name: string;
team: string; team: string;
country: string; country: string;
races: number; races: number;
formattedRaces: string;
impressions: number; impressions: number;
formattedImpressions: string; formattedImpressions: string;
}>; }>;
@@ -98,6 +108,7 @@ export interface SponsorLeagueDetailViewData extends ViewData {
formattedDate: string; formattedDate: string;
status: 'completed' | 'upcoming'; status: 'completed' | 'upcoming';
views: number; views: number;
formattedViews: string;
}>; }>;
} }
@@ -139,13 +150,13 @@ export function SponsorLeagueDetailTemplate({
}, },
{ {
label: 'Engagement', label: 'Engagement',
value: `${league.engagement}%`, value: league.formattedEngagement,
icon: BarChart3, icon: BarChart3,
variant: 'warning', variant: 'warning',
}, },
{ {
label: 'Races Left', label: 'Races Left',
value: league.racesLeft, value: league.formattedRacesLeft,
icon: Calendar, icon: Calendar,
variant: 'default', variant: 'default',
}, },
@@ -156,6 +167,7 @@ export function SponsorLeagueDetailTemplate({
id: 'main', id: 'main',
name: 'Main Sponsor', name: 'Main Sponsor',
price: league.sponsorSlots.main.price, price: league.sponsorSlots.main.price,
priceLabel: league.sponsorSlots.main.priceLabel,
period: 'Season', period: 'Season',
description: 'Exclusive primary branding across all league assets.', description: 'Exclusive primary branding across all league assets.',
features: league.sponsorSlots.main.benefits, features: league.sponsorSlots.main.benefits,
@@ -166,6 +178,7 @@ export function SponsorLeagueDetailTemplate({
id: 'secondary', id: 'secondary',
name: 'Secondary Sponsor', name: 'Secondary Sponsor',
price: league.sponsorSlots.secondary.price, price: league.sponsorSlots.secondary.price,
priceLabel: league.sponsorSlots.secondary.priceLabel,
period: 'Season', period: 'Season',
description: 'Supporting branding on cars and broadcast overlays.', description: 'Supporting branding on cars and broadcast overlays.',
features: league.sponsorSlots.secondary.benefits, features: league.sponsorSlots.secondary.benefits,
@@ -238,8 +251,8 @@ export function SponsorLeagueDetailTemplate({
<InfoRow label="Platform" value={league.game} /> <InfoRow label="Platform" value={league.game} />
<InfoRow label="Season" value={league.season} /> <InfoRow label="Season" value={league.season} />
<InfoRow label="Duration" value="Oct 2025 - Feb 2026" /> <InfoRow label="Duration" value="Oct 2025 - Feb 2026" />
<InfoRow label="Drivers" value={league.drivers} /> <InfoRow label="Drivers" value={league.formattedDrivers} />
<InfoRow label="Races" value={league.races} last /> <InfoRow label="Races" value={league.formattedRaces} last />
</SharedStack> </SharedStack>
</SharedCard> </SharedCard>
@@ -253,8 +266,8 @@ export function SponsorLeagueDetailTemplate({
<InfoRow label="Total Season Views" value={league.formattedTotalImpressions} /> <InfoRow label="Total Season Views" value={league.formattedTotalImpressions} />
<InfoRow label="Projected Total" value={league.formattedProjectedTotal} /> <InfoRow label="Projected Total" value={league.formattedProjectedTotal} />
<InfoRow label="Main Sponsor CPM" value={league.formattedMainSponsorCpm} color="text-performance-green" /> <InfoRow label="Main Sponsor CPM" value={league.formattedMainSponsorCpm} color="text-performance-green" />
<InfoRow label="Engagement Rate" value={`${league.engagement}%`} /> <InfoRow label="Engagement Rate" value={league.formattedEngagement} />
<InfoRow label="League Rating" value={`${league.rating}/5.0`} last /> <InfoRow label="League Rating" value={league.formattedRating} last />
</SharedStack> </SharedStack>
</SharedCard> </SharedCard>
@@ -274,7 +287,7 @@ export function SponsorLeagueDetailTemplate({
</Surface> </Surface>
<SharedBox> <SharedBox>
<SharedText size="lg" weight="semibold" color="text-white" block>{league.nextRace.name}</SharedText> <SharedText size="lg" weight="semibold" color="text-white" block>{league.nextRace.name}</SharedText>
<SharedText size="sm" color="text-gray-400" block mt={1}>{league.nextRace.date}</SharedText> <SharedText size="sm" color="text-gray-400" block mt={1}>{league.nextRace.formattedDate}</SharedText>
</SharedBox> </SharedBox>
</SharedStack> </SharedStack>
<SharedButton variant="secondary"> <SharedButton variant="secondary">
@@ -300,7 +313,7 @@ export function SponsorLeagueDetailTemplate({
<SharedStack direction="row" align="center" justify="between"> <SharedStack direction="row" align="center" justify="between">
<SharedStack direction="row" align="center" gap={4}> <SharedStack direction="row" align="center" gap={4}>
<Surface variant="muted" rounded="full" padding={1} style={{ width: '2.5rem', height: '2.5rem', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#262626' }}> <Surface variant="muted" rounded="full" padding={1} style={{ width: '2.5rem', height: '2.5rem', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#262626' }}>
<SharedText weight="bold" color="text-white">{driver.position}</SharedText> <SharedText weight="bold" color="text-white">{driver.positionLabel}</SharedText>
</Surface> </Surface>
<SharedBox> <SharedBox>
<SharedText weight="medium" color="text-white" block>{driver.name}</SharedText> <SharedText weight="medium" color="text-white" block>{driver.name}</SharedText>
@@ -309,7 +322,7 @@ export function SponsorLeagueDetailTemplate({
</SharedStack> </SharedStack>
<SharedStack direction="row" align="center" gap={8}> <SharedStack direction="row" align="center" gap={8}>
<SharedBox textAlign="right"> <SharedBox textAlign="right">
<SharedText weight="medium" color="text-white" block>{driver.races}</SharedText> <SharedText weight="medium" color="text-white" block>{driver.formattedRaces}</SharedText>
<SharedText size="xs" color="text-gray-500">races</SharedText> <SharedText size="xs" color="text-gray-500">races</SharedText>
</SharedBox> </SharedBox>
<SharedBox textAlign="right"> <SharedBox textAlign="right">
@@ -344,7 +357,7 @@ export function SponsorLeagueDetailTemplate({
<SharedBox> <SharedBox>
{race.status === 'completed' ? ( {race.status === 'completed' ? (
<SharedBox textAlign="right"> <SharedBox textAlign="right">
<SharedText weight="semibold" color="text-white" block>{race.views.toLocaleString()}</SharedText> <SharedText weight="semibold" color="text-white" block>{race.formattedViews}</SharedText>
<SharedText size="xs" color="text-gray-500">views</SharedText> <SharedText size="xs" color="text-gray-500">views</SharedText>
</SharedBox> </SharedBox>
) : ( ) : (
@@ -384,7 +397,7 @@ export function SponsorLeagueDetailTemplate({
<SharedStack gap={3} mb={6}> <SharedStack gap={3} mb={6}>
<InfoRow label="Selected Tier" value={`${selectedTier.charAt(0).toUpperCase() + selectedTier.slice(1)} Sponsor`} /> <InfoRow label="Selected Tier" value={`${selectedTier.charAt(0).toUpperCase() + selectedTier.slice(1)} Sponsor`} />
<InfoRow label="Season Price" value={`$${selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price}`} /> <InfoRow label="Season Price" value={selectedTier === 'main' ? league.sponsorSlots.main.priceLabel : league.sponsorSlots.secondary.priceLabel} />
<InfoRow label={`Platform Fee (${siteConfig.fees.platformFeePercent}%)`} value={`$${((selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price) * siteConfig.fees.platformFeePercent / 100).toFixed(2)}`} /> <InfoRow label={`Platform Fee (${siteConfig.fees.platformFeePercent}%)`} value={`$${((selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price) * siteConfig.fees.platformFeePercent / 100).toFixed(2)}`} />
<SharedBox pt={4} borderTop borderColor="border-neutral-800"> <SharedBox pt={4} borderTop borderColor="border-neutral-800">
<SharedStack direction="row" align="center" justify="between"> <SharedStack direction="row" align="center" justify="between">

View File

@@ -96,9 +96,9 @@ export function TeamDetailTemplate({
entityName={team.name} entityName={team.name}
tier="standard" tier="standard"
metrics={viewData.teamMetrics} metrics={viewData.teamMetrics}
slots={SlotTemplates.team(true, true, 500, 250)} slots={SlotTemplates.team(true, true, '$500', '$250')}
trustScore={90} trustScoreLabel="90/100"
monthlyActivity={85} monthlyActivityLabel="85%"
onNavigate={(href) => window.location.href = href} onNavigate={(href) => window.location.href = href}
/> />
)} )}

View File

@@ -33,6 +33,9 @@ export function TeamsTemplate({ viewData, onTeamClick, onViewFullLeaderboard, on
name={team.teamName} name={team.teamName}
memberCount={team.memberCount} memberCount={team.memberCount}
logo={team.logoUrl} logo={team.logoUrl}
ratingLabel={team.ratingLabel}
winsLabel={team.winsLabel}
racesLabel={team.racesLabel}
onClick={() => onTeamClick?.(team.teamId)} onClick={() => onTeamClick?.(team.teamId)}
/> />
))} ))}

View File

@@ -1,106 +1,2 @@
adapters/bootstrap/SeedDemoUsers.ts(6,1): error TS6133: 'UserRepository' is declared but its value is never read. apps/website/components/teams/TeamAdmin.tsx(177,17): error TS2322: Type '{ key: string; driverId: string; requestedAt: string; onApprove: () => void; onReject: () => void; isApproving: boolean; isRejecting: boolean; }' is not assignable to type 'IntrinsicAttributes & JoinRequestItemProps'.
apps/api/src/domain/analytics/AnalyticsProviders.ts(3,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'. Property 'requestedAt' does not exist on type 'IntrinsicAttributes & JoinRequestItemProps'.
apps/api/src/domain/auth/AuthProviders.ts(16,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
apps/api/src/domain/auth/AuthService.ts(3,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
apps/api/src/domain/bootstrap/BootstrapModule.ts(1,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
apps/api/src/domain/bootstrap/BootstrapProviders.ts(8,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
apps/api/src/domain/dashboard/DashboardProviders.ts(11,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
apps/api/src/domain/dashboard/DashboardService.ts(7,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
apps/api/src/domain/driver/DriverProviders.ts(167,14): error TS2693: 'RankingUseCase' only refers to a type, but is being used as a value here.
apps/api/src/domain/driver/DriverProviders.ts(176,14): error TS2693: 'DriverStatsUseCase' only refers to a type, but is being used as a value here.
apps/api/src/domain/driver/DriverService.ts(33,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
apps/api/src/domain/league/LeagueProviders.ts(16,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
apps/api/src/domain/league/LeagueService.ts(61,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
apps/api/src/domain/media/MediaController.ts(3,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
apps/api/src/domain/media/MediaProviders.ts(7,10): error TS2724: '"@core/media/domain/repositories/AvatarGenerationRepository"' has no exported member named 'IAvatarGenerationRepository'. Did you mean 'AvatarGenerationRepository'?
apps/api/src/domain/media/MediaProviders.ts(8,10): error TS2724: '"@core/media/domain/repositories/AvatarRepository"' has no exported member named 'IAvatarRepository'. Did you mean 'AvatarRepository'?
apps/api/src/domain/media/MediaProviders.ts(9,10): error TS2724: '"@core/media/domain/repositories/MediaRepository"' has no exported member named 'IMediaRepository'. Did you mean 'MediaRepository'?
apps/api/src/domain/media/MediaProviders.ts(11,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
apps/api/src/domain/media/MediaService.ts(39,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
apps/api/src/domain/notifications/NotificationsProviders.ts(5,10): error TS2724: '"@core/notifications/domain/repositories/NotificationRepository"' has no exported member named 'INotificationRepository'. Did you mean 'NotificationRepository'?
apps/api/src/domain/notifications/NotificationsProviders.ts(6,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
apps/api/src/domain/payments/PaymentsProviders.ts(4,15): error TS2724: '"@core/payments/domain/repositories/MembershipFeeRepository"' has no exported member named 'IMemberPaymentRepository'. Did you mean 'MemberPaymentRepository'?
apps/api/src/domain/payments/PaymentsProviders.ts(4,41): error TS2724: '"@core/payments/domain/repositories/MembershipFeeRepository"' has no exported member named 'IMembershipFeeRepository'. Did you mean 'MembershipFeeRepository'?
apps/api/src/domain/payments/PaymentsProviders.ts(5,15): error TS2724: '"@core/payments/domain/repositories/PaymentRepository"' has no exported member named 'IPaymentRepository'. Did you mean 'PaymentRepository'?
apps/api/src/domain/payments/PaymentsProviders.ts(6,15): error TS2724: '"@core/payments/domain/repositories/PrizeRepository"' has no exported member named 'IPrizeRepository'. Did you mean 'PrizeRepository'?
apps/api/src/domain/payments/PaymentsProviders.ts(7,15): error TS2724: '"@core/payments/domain/repositories/WalletRepository"' has no exported member named 'ITransactionRepository'. Did you mean 'TransactionRepository'?
apps/api/src/domain/payments/PaymentsProviders.ts(7,39): error TS2724: '"@core/payments/domain/repositories/WalletRepository"' has no exported member named 'IWalletRepository'. Did you mean 'WalletRepository'?
apps/api/src/domain/payments/PaymentsService.ts(1,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
apps/api/src/domain/protests/ProtestsProviders.ts(4,15): error TS2724: '"@core/racing/domain/repositories/LeagueMembershipRepository"' has no exported member named 'ILeagueMembershipRepository'. Did you mean 'LeagueMembershipRepository'?
apps/api/src/domain/protests/ProtestsProviders.ts(5,15): error TS2724: '"@core/racing/domain/repositories/ProtestRepository"' has no exported member named 'IProtestRepository'. Did you mean 'ProtestRepository'?
apps/api/src/domain/protests/ProtestsProviders.ts(6,15): error TS2724: '"@core/racing/domain/repositories/RaceRepository"' has no exported member named 'IRaceRepository'. Did you mean 'RaceRepository'?
apps/api/src/domain/protests/ProtestsProviders.ts(7,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
apps/api/src/domain/protests/ProtestsService.test.ts(6,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
apps/api/src/domain/protests/ProtestsService.ts(1,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
apps/api/src/domain/race/presenters/RaceDetailPresenter.ts(2,15): error TS2724: '"@core/racing/application/ports/ImageServicePort"' has no exported member named 'IImageServicePort'. Did you mean 'ImageServicePort'?
apps/api/src/domain/race/presenters/RacePenaltiesPresenter.ts(15,74): error TS2352: Conversion of type '{ id: PenaltyId; driverId: string; type: string; value: number; reason: string; issuedBy: string; issuedAt: string; notes: string | undefined; }' to type 'RacePenaltyDTO' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
Types of property 'id' are incompatible.
Type 'PenaltyId' is not comparable to type 'string'.
apps/api/src/domain/race/presenters/RaceResultsDetailPresenter.ts(1,15): error TS2724: '"@core/racing/application/ports/ImageServicePort"' has no exported member named 'IImageServicePort'. Did you mean 'ImageServicePort'?
apps/api/src/domain/race/RaceProviders.ts(5,15): error TS2724: '"@core/racing/domain/repositories/DriverRepository"' has no exported member named 'IDriverRepository'. Did you mean 'DriverRepository'?
apps/api/src/domain/race/RaceProviders.ts(6,15): error TS2724: '"@core/racing/domain/repositories/LeagueMembershipRepository"' has no exported member named 'ILeagueMembershipRepository'. Did you mean 'LeagueMembershipRepository'?
apps/api/src/domain/race/RaceProviders.ts(7,15): error TS2724: '"@core/racing/domain/repositories/LeagueRepository"' has no exported member named 'ILeagueRepository'. Did you mean 'LeagueRepository'?
apps/api/src/domain/race/RaceProviders.ts(8,15): error TS2724: '"@core/racing/domain/repositories/PenaltyRepository"' has no exported member named 'IPenaltyRepository'. Did you mean 'PenaltyRepository'?
apps/api/src/domain/race/RaceProviders.ts(9,15): error TS2724: '"@core/racing/domain/repositories/ProtestRepository"' has no exported member named 'IProtestRepository'. Did you mean 'ProtestRepository'?
apps/api/src/domain/race/RaceProviders.ts(10,15): error TS2724: '"@core/racing/domain/repositories/RaceRegistrationRepository"' has no exported member named 'IRaceRegistrationRepository'. Did you mean 'RaceRegistrationRepository'?
apps/api/src/domain/race/RaceProviders.ts(11,15): error TS2724: '"@core/racing/domain/repositories/RaceRepository"' has no exported member named 'IRaceRepository'. Did you mean 'RaceRepository'?
apps/api/src/domain/race/RaceProviders.ts(12,15): error TS2724: '"@core/racing/domain/repositories/ResultRepository"' has no exported member named 'IResultRepository'. Did you mean 'ResultRepository'?
apps/api/src/domain/race/RaceProviders.ts(13,15): error TS2724: '"@core/racing/domain/repositories/StandingRepository"' has no exported member named 'IStandingRepository'. Did you mean 'StandingRepository'?
apps/api/src/domain/race/RaceProviders.ts(14,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
apps/api/src/domain/race/RaceService.ts(11,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
apps/api/src/domain/sponsor/SponsorProviders.ts(6,15): error TS2724: '"@core/payments/domain/repositories/PaymentRepository"' has no exported member named 'IPaymentRepository'. Did you mean 'PaymentRepository'?
apps/api/src/domain/sponsor/SponsorProviders.ts(7,15): error TS2724: '"@core/payments/domain/repositories/WalletRepository"' has no exported member named 'IWalletRepository'. Did you mean 'WalletRepository'?
apps/api/src/domain/sponsor/SponsorProviders.ts(8,10): error TS2724: '"@core/racing/domain/repositories/LeagueMembershipRepository"' has no exported member named 'ILeagueMembershipRepository'. Did you mean 'LeagueMembershipRepository'?
apps/api/src/domain/sponsor/SponsorProviders.ts(9,10): error TS2724: '"@core/racing/domain/repositories/LeagueRepository"' has no exported member named 'ILeagueRepository'. Did you mean 'LeagueRepository'?
apps/api/src/domain/sponsor/SponsorProviders.ts(10,10): error TS2724: '"@core/racing/domain/repositories/LeagueWalletRepository"' has no exported member named 'ILeagueWalletRepository'. Did you mean 'LeagueWalletRepository'?
apps/api/src/domain/sponsor/SponsorProviders.ts(11,10): error TS2724: '"@core/racing/domain/repositories/RaceRepository"' has no exported member named 'IRaceRepository'. Did you mean 'RaceRepository'?
apps/api/src/domain/sponsor/SponsorProviders.ts(12,10): error TS2724: '"@core/racing/domain/repositories/SeasonRepository"' has no exported member named 'ISeasonRepository'. Did you mean 'SeasonRepository'?
apps/api/src/domain/sponsor/SponsorProviders.ts(13,10): error TS2724: '"@core/racing/domain/repositories/SeasonSponsorshipRepository"' has no exported member named 'ISeasonSponsorshipRepository'. Did you mean 'SeasonSponsorshipRepository'?
apps/api/src/domain/sponsor/SponsorProviders.ts(14,10): error TS2724: '"@core/racing/domain/repositories/SponsorRepository"' has no exported member named 'ISponsorRepository'. Did you mean 'SponsorRepository'?
apps/api/src/domain/sponsor/SponsorProviders.ts(15,10): error TS2724: '"@core/racing/domain/repositories/SponsorshipPricingRepository"' has no exported member named 'ISponsorshipPricingRepository'. Did you mean 'SponsorshipPricingRepository'?
apps/api/src/domain/sponsor/SponsorProviders.ts(16,10): error TS2724: '"@core/racing/domain/repositories/SponsorshipRequestRepository"' has no exported member named 'ISponsorshipRequestRepository'. Did you mean 'SponsorshipRequestRepository'?
apps/api/src/domain/sponsor/SponsorProviders.ts(17,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
apps/api/src/domain/sponsor/SponsorService.test.ts(13,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
apps/api/src/domain/sponsor/SponsorService.ts(38,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
apps/api/src/domain/team/TeamProviders.ts(33,15): error TS2724: '"@core/racing/domain/repositories/DriverRepository"' has no exported member named 'IDriverRepository'. Did you mean 'DriverRepository'?
apps/api/src/domain/team/TeamProviders.ts(34,15): error TS2724: '"@core/racing/domain/repositories/TeamMembershipRepository"' has no exported member named 'ITeamMembershipRepository'. Did you mean 'TeamMembershipRepository'?
apps/api/src/domain/team/TeamProviders.ts(35,15): error TS2724: '"@core/racing/domain/repositories/TeamRepository"' has no exported member named 'ITeamRepository'. Did you mean 'TeamRepository'?
apps/api/src/domain/team/TeamProviders.ts(36,15): error TS2724: '"@core/racing/domain/repositories/TeamStatsRepository"' has no exported member named 'ITeamStatsRepository'. Did you mean 'TeamStatsRepository'?
apps/api/src/domain/team/TeamProviders.ts(37,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
apps/api/src/domain/team/TeamService.test.ts(9,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
apps/api/src/domain/team/TeamService.ts(14,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
apps/api/src/persistence/achievement/AchievementPersistenceModule.test.ts(1,15): error TS2724: '"@core/identity/domain/repositories/AchievementRepository"' has no exported member named 'IAchievementRepository'. Did you mean 'AchievementRepository'?
apps/api/src/persistence/inmemory/InMemoryAchievementPersistenceModule.ts(5,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
apps/api/src/persistence/inmemory/InMemoryAnalyticsPersistenceModule.ts(5,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
apps/api/src/persistence/inmemory/InMemoryIdentityPersistenceModule.ts(7,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
apps/api/src/persistence/inmemory/InMemoryMediaPersistenceModule.ts(5,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
apps/api/src/persistence/inmemory/InMemoryMediaPersistenceModule.ts(7,15): error TS2724: '"@core/media/domain/repositories/AvatarGenerationRepository"' has no exported member named 'IAvatarGenerationRepository'. Did you mean 'AvatarGenerationRepository'?
apps/api/src/persistence/inmemory/InMemoryMediaPersistenceModule.ts(8,15): error TS2724: '"@core/media/domain/repositories/AvatarRepository"' has no exported member named 'IAvatarRepository'. Did you mean 'AvatarRepository'?
apps/api/src/persistence/inmemory/InMemoryMediaPersistenceModule.ts(9,15): error TS2724: '"@core/media/domain/repositories/MediaRepository"' has no exported member named 'IMediaRepository'. Did you mean 'MediaRepository'?
apps/api/src/persistence/inmemory/InMemoryNotificationsPersistenceModule.ts(5,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
apps/api/src/persistence/inmemory/InMemoryNotificationsPersistenceModule.ts(9,15): error TS2724: '"@core/notifications/domain/repositories/NotificationPreferenceRepository"' has no exported member named 'INotificationPreferenceRepository'. Did you mean 'NotificationPreferenceRepository'?
apps/api/src/persistence/inmemory/InMemoryNotificationsPersistenceModule.ts(10,15): error TS2724: '"@core/notifications/domain/repositories/NotificationRepository"' has no exported member named 'INotificationRepository'. Did you mean 'NotificationRepository'?
apps/api/src/persistence/inmemory/InMemoryPaymentsPersistenceModule.ts(5,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
apps/api/src/persistence/inmemory/InMemoryPaymentsPersistenceModule.ts(8,5): error TS2724: '"@core/payments/domain/repositories/MembershipFeeRepository"' has no exported member named 'IMemberPaymentRepository'. Did you mean 'MemberPaymentRepository'?
apps/api/src/persistence/inmemory/InMemoryPaymentsPersistenceModule.ts(9,5): error TS2724: '"@core/payments/domain/repositories/MembershipFeeRepository"' has no exported member named 'IMembershipFeeRepository'. Did you mean 'MembershipFeeRepository'?
apps/api/src/persistence/inmemory/InMemoryPaymentsPersistenceModule.ts(11,15): error TS2724: '"@core/payments/domain/repositories/PaymentRepository"' has no exported member named 'IPaymentRepository'. Did you mean 'PaymentRepository'?
apps/api/src/persistence/inmemory/InMemoryPaymentsPersistenceModule.ts(12,15): error TS2724: '"@core/payments/domain/repositories/PrizeRepository"' has no exported member named 'IPrizeRepository'. Did you mean 'PrizeRepository'?
apps/api/src/persistence/inmemory/InMemoryPaymentsPersistenceModule.ts(13,15): error TS2724: '"@core/payments/domain/repositories/WalletRepository"' has no exported member named 'ITransactionRepository'. Did you mean 'TransactionRepository'?
apps/api/src/persistence/inmemory/InMemoryPaymentsPersistenceModule.ts(13,39): error TS2724: '"@core/payments/domain/repositories/WalletRepository"' has no exported member named 'IWalletRepository'. Did you mean 'WalletRepository'?
apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts(5,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
apps/api/src/persistence/inmemory/InMemorySocialPersistenceModule.ts(5,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
apps/api/src/persistence/inmemory/InMemorySocialPersistenceModule.ts(7,15): error TS2724: '"@core/social/domain/repositories/FeedRepository"' has no exported member named 'IFeedRepository'. Did you mean 'FeedRepository'?
apps/api/src/persistence/inmemory/InMemorySocialPersistenceModule.ts(8,15): error TS2724: '"@core/social/domain/repositories/SocialGraphRepository"' has no exported member named 'ISocialGraphRepository'. Did you mean 'SocialGraphRepository'?
apps/api/src/persistence/postgres/PostgresIdentityPersistenceModule.ts(1,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
apps/api/src/persistence/postgres/PostgresNotificationsPersistenceModule.ts(7,15): error TS2724: '"@core/notifications/domain/repositories/NotificationPreferenceRepository"' has no exported member named 'INotificationPreferenceRepository'. Did you mean 'NotificationPreferenceRepository'?
apps/api/src/persistence/postgres/PostgresNotificationsPersistenceModule.ts(8,15): error TS2724: '"@core/notifications/domain/repositories/NotificationRepository"' has no exported member named 'INotificationRepository'. Did you mean 'NotificationRepository'?
apps/api/src/persistence/postgres/PostgresNotificationsPersistenceModule.ts(9,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
apps/api/src/persistence/postgres/PostgresPaymentsPersistenceModule.ts(7,15): error TS2724: '"@core/payments/domain/repositories/MembershipFeeRepository"' has no exported member named 'IMemberPaymentRepository'. Did you mean 'MemberPaymentRepository'?
apps/api/src/persistence/postgres/PostgresPaymentsPersistenceModule.ts(7,41): error TS2724: '"@core/payments/domain/repositories/MembershipFeeRepository"' has no exported member named 'IMembershipFeeRepository'. Did you mean 'MembershipFeeRepository'?
apps/api/src/persistence/postgres/PostgresPaymentsPersistenceModule.ts(8,15): error TS2724: '"@core/payments/domain/repositories/PaymentRepository"' has no exported member named 'IPaymentRepository'. Did you mean 'PaymentRepository'?
apps/api/src/persistence/postgres/PostgresPaymentsPersistenceModule.ts(9,15): error TS2724: '"@core/payments/domain/repositories/PrizeRepository"' has no exported member named 'IPrizeRepository'. Did you mean 'PrizeRepository'?
apps/api/src/persistence/postgres/PostgresPaymentsPersistenceModule.ts(10,15): error TS2724: '"@core/payments/domain/repositories/WalletRepository"' has no exported member named 'ITransactionRepository'. Did you mean 'TransactionRepository'?
apps/api/src/persistence/postgres/PostgresPaymentsPersistenceModule.ts(10,39): error TS2724: '"@core/payments/domain/repositories/WalletRepository"' has no exported member named 'IWalletRepository'. Did you mean 'WalletRepository'?
apps/api/src/persistence/postgres/PostgresRacingPersistenceModule.ts(116,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
apps/api/src/persistence/social/PostgresSocialPersistence.integration.test.ts(13,15): error TS2724: '"@core/social/domain/repositories/FeedRepository"' has no exported member named 'IFeedRepository'. Did you mean 'FeedRepository'?
apps/api/src/persistence/social/PostgresSocialPersistence.integration.test.ts(14,15): error TS2724: '"@core/social/domain/repositories/SocialGraphRepository"' has no exported member named 'ISocialGraphRepository'. Did you mean 'SocialGraphRepository'?