do to formatters

This commit is contained in:
2026-01-24 01:07:43 +01:00
parent ae59df61eb
commit 891b3cf0ee
140 changed files with 656 additions and 1159 deletions

View File

@@ -1,11 +1,11 @@
import { LeagueDetailPageQuery } from '@/lib/page-queries/LeagueDetailPageQuery';
import { notFound } from 'next/navigation';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { RosterTable } from '@/components/leagues/RosterTable'; import { RosterTable } from '@/components/leagues/RosterTable';
import { LeagueDetailPageQuery } from '@/lib/page-queries/LeagueDetailPageQuery';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { notFound } from 'next/navigation';
import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { DateFormatter } from '@/lib/formatters/DateFormatter';
interface Props { interface Props {
params: Promise<{ id: string }>; params: Promise<{ id: string }>;
@@ -25,7 +25,7 @@ export default async function LeagueRosterPage({ params }: Props) {
driverName: m.driver.name, driverName: m.driver.name,
role: m.role, role: m.role,
joinedAt: m.joinedAt, joinedAt: m.joinedAt,
joinedAtLabel: DateDisplay.formatShort(m.joinedAt) joinedAtLabel: DateFormatter.formatShort(m.joinedAt)
})); }));
return ( return (

View File

@@ -1,89 +1,3 @@
'use client'; 'use client';
import { useSponsorSponsorships } from "@/hooks/sponsor/useSponsorSponsorships";
import { SponsorCampaignsTemplate, SponsorshipType, SponsorCampaignsViewData } from "@/templates/SponsorCampaignsTemplate";
import { Box } from "@/ui/Box";
import { Button } from "@/ui/Button";
import { Text } from "@/ui/Text";
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() {
const [typeFilter, setTypeFilter] = useState<SponsorshipType>('all');
const [searchQuery, setSearchQuery] = useState('');
const { data: sponsorshipsData, isLoading, error, retry } = useSponsorSponsorships('demo-sponsor-1');
if (isLoading) {
return (
<Box maxWidth="7xl" mx="auto" py={8} px={4} display="flex" alignItems="center" justifyContent="center" minHeight="400px">
<Box textAlign="center">
<Box w="8" h="8" border borderTop={false} borderColor="border-primary-blue" rounded="full" animate="spin" mx="auto" mb={4} />
<Text color="text-gray-400">Loading sponsorships...</Text>
</Box>
</Box>
);
}
if (error || !sponsorshipsData) {
return (
<Box maxWidth="7xl" mx="auto" py={8} px={4} display="flex" alignItems="center" justifyContent="center" minHeight="400px">
<Box textAlign="center">
<Text color="text-gray-400">{error?.getUserMessage() || 'No sponsorships data available'}</Text>
{error && (
<Button variant="secondary" onClick={retry} mt={4}>
Retry
</Button>
)}
</Box>
</Box>
);
}
// 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 = {
total: sponsorshipsData.sponsorships.length,
active: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'active').length,
pending: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'pending_approval').length,
approved: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'approved').length,
rejected: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'rejected').length,
formattedTotalInvestment: CurrencyDisplay.format(totalInvestment),
formattedTotalImpressions: NumberDisplay.formatCompact(totalImpressions),
};
const sponsorships = sponsorshipsData.sponsorships.map((s: any) => ({
...s,
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 = sponsorships.filter((s: any) => {
// For now, we only have leagues in the DTO
if (typeFilter !== 'all' && typeFilter !== 'leagues') return false;
if (searchQuery && !s.leagueName.toLowerCase().includes(searchQuery.toLowerCase())) return false;
return true;
});
return (
<SponsorCampaignsTemplate
viewData={viewData}
filteredSponsorships={filteredSponsorships as any}
typeFilter={typeFilter}
setTypeFilter={setTypeFilter}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
/>
);
}

View File

@@ -1,27 +1,27 @@
'use client'; 'use client';
import type { MembershipRole } from '@/lib/types/MembershipRole';
import { useParams } from 'next/navigation';
import { useMemo } from 'react';
import { import {
useApproveJoinRequest,
useLeagueJoinRequests, useLeagueJoinRequests,
useLeagueRosterAdmin, useLeagueRosterAdmin,
useApproveJoinRequest,
useRejectJoinRequest, useRejectJoinRequest,
useUpdateMemberRole,
useRemoveMember, useRemoveMember,
useUpdateMemberRole,
} from "@/hooks/league/useLeagueRosterAdmin"; } from "@/hooks/league/useLeagueRosterAdmin";
import { RosterAdminTemplate } from '@/templates/RosterAdminTemplate'; import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
import type { JoinRequestData, RosterMemberData, LeagueRosterAdminViewData } from '@/lib/view-data/LeagueRosterAdminViewData';
import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO'; import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO';
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO'; import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts'; import type { MembershipRole } from '@/lib/types/MembershipRole';
import type { JoinRequestData, LeagueRosterAdminViewData, RosterMemberData } from '@/lib/view-data/LeagueRosterAdminViewData';
import { RosterAdminTemplate } from '@/templates/RosterAdminTemplate';
import { useParams } from 'next/navigation';
import { useMemo } from 'react';
import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { DateFormatter } from '@/lib/formatters/DateFormatter';
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({ }: Partial<ClientWrapperProps<LeagueRosterAdminViewData>>) {
const params = useParams(); const params = useParams();
const leagueId = params.id as string; const leagueId = params.id as string;
@@ -83,7 +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), formattedRequestedAt: DateFormatter.formatShort(req.requestedAt),
message: req.message || undefined, message: req.message || undefined,
})), })),
members: members.map((m: LeagueRosterMemberDTO): RosterMemberData => ({ members: members.map((m: LeagueRosterMemberDTO): RosterMemberData => ({
@@ -91,7 +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), formattedJoinedAt: DateFormatter.formatShort(m.joinedAt),
})), })),
}), [leagueId, joinRequests, members]); }), [leagueId, joinRequests, members]);

View File

@@ -1,8 +1,8 @@
import { DateFormatter } from '@/lib/formatters/DateFormatter';
import { Card } from '@/ui/Card'; import { Card } from '@/ui/Card';
import { Text } from '@/ui/Text';
import { Group } from '@/ui/Group'; import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { Text } from '@/ui/Text';
interface AchievementCardProps { interface AchievementCardProps {
title: string; title: string;
@@ -36,7 +36,7 @@ export function AchievementCard({
<Text weight="medium" variant="high">{title}</Text> <Text weight="medium" variant="high">{title}</Text>
<Text size="xs" variant="med">{description}</Text> <Text size="xs" variant="med">{description}</Text>
<Text size="xs" variant="low"> <Text size="xs" variant="low">
{DateDisplay.formatShort(unlockedAt)} {DateFormatter.formatShort(unlockedAt)}
</Text> </Text>
</Stack> </Stack>
</Group> </Group>

View File

@@ -1,15 +1,13 @@
import { AchievementDisplay } from '@/lib/display-objects/AchievementDisplay'; import { AchievementFormatter } from '@/lib/formatters/AchievementFormatter';
import { Card } from '@/ui/Card'; import { Card } from '@/ui/Card';
import { Grid } from '@/ui/Grid'; import { Grid } from '@/ui/Grid';
import { Group } from '@/ui/Group';
import { Heading } from '@/ui/Heading'; import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon'; import { Icon } from '@/ui/Icon';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
import { Surface } from '@/ui/Surface'; import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { Award, Crown, Medal, Star, Target, Trophy, Zap } from 'lucide-react'; import { Award, Crown, Medal, Star, Target, Trophy, Zap } from 'lucide-react';
import React from 'react';
interface Achievement { interface Achievement {
id: string; id: string;
@@ -53,7 +51,7 @@ export function AchievementGrid({ achievements }: AchievementGridProps) {
<Grid cols={1} gap={4}> <Grid cols={1} gap={4}>
{achievements.map((achievement) => { {achievements.map((achievement) => {
const AchievementIcon = getAchievementIcon(achievement.icon); const AchievementIcon = getAchievementIcon(achievement.icon);
const rarity = AchievementDisplay.getRarityVariant(achievement.rarity); const rarity = AchievementFormatter.getRarityVariant(achievement.rarity);
return ( return (
<Card <Card
key={achievement.id} key={achievement.id}

View File

@@ -1,26 +1,24 @@
'use client'; 'use client';
import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { DateFormatter } from '@/lib/formatters/DateFormatter';
import { AdminUsersViewData } from '@/lib/view-data/AdminUsersViewData'; import { AdminUsersViewData } from '@/lib/view-data/AdminUsersViewData';
import { Badge } from '@/ui/Badge';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { DriverIdentity } from '@/ui/DriverIdentity';
import { Group } from '@/ui/Group';
import { IconButton } from '@/ui/IconButton'; import { IconButton } from '@/ui/IconButton';
import { SimpleCheckbox } from '@/ui/SimpleCheckbox'; import { SimpleCheckbox } from '@/ui/SimpleCheckbox';
import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { DriverIdentity } from '@/ui/DriverIdentity';
import { import {
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableHead, TableHead,
TableHeader, TableHeader,
TableRow TableRow
} from '@/ui/Table'; } from '@/ui/Table';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { MoreVertical, Trash2 } from 'lucide-react'; import { MoreVertical, Trash2 } from 'lucide-react';
import { UserStatusTag } from './UserStatusTag'; import { UserStatusTag } from './UserStatusTag';
import React from 'react';
interface AdminUsersTableProps { interface AdminUsersTableProps {
users: AdminUsersViewData['users']; users: AdminUsersViewData['users'];
@@ -102,7 +100,7 @@ export function AdminUsersTable({
</TableCell> </TableCell>
<TableCell> <TableCell>
<Text size="sm" variant="low"> <Text size="sm" variant="low">
{user.lastLoginAt ? DateDisplay.formatShort(user.lastLoginAt) : 'Never'} {user.lastLoginAt ? DateFormatter.formatShort(user.lastLoginAt) : 'Never'}
</Text> </Text>
</TableCell> </TableCell>
<TableCell> <TableCell>

View File

@@ -1,6 +1,6 @@
import { CountryFlagDisplay } from '@/lib/display-objects/CountryFlagDisplay'; import { CountryFlagFormatter } from '@/lib/formatters/CountryFlagFormatter';
import { Badge } from '@/ui/Badge'; import { Badge } from '@/ui/Badge';
import { Icon } from '@/ui/Icon'; import { Icon } from '@/ui/Icon';
import { Image } from '@/ui/Image'; import { Image } from '@/ui/Image';
@@ -88,7 +88,7 @@ export function DriverEntryRow({
justifyContent="center" justifyContent="center"
fontSize="0.625rem" fontSize="0.625rem"
> >
{CountryFlagDisplay.fromCountryCode(country).toString()} {CountryFlagFormatter.fromCountryCode(country).toString()}
</Stack> </Stack>
</Stack> </Stack>

View File

@@ -1,16 +1,16 @@
import { mediaConfig } from '@/lib/config/mediaConfig'; import { mediaConfig } from '@/lib/config/mediaConfig';
import { CountryFlagDisplay } from '@/lib/display-objects/CountryFlagDisplay'; import { CountryFlagFormatter } from '@/lib/formatters/CountryFlagFormatter';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading'; import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Image } from '@/ui/Image'; import { Image } from '@/ui/Image';
import { Link } from '@/ui/Link'; import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface'; import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Icon } from '@/ui/Icon';
import { Calendar, Clock, ExternalLink, Globe, Star, Trophy, UserPlus } from 'lucide-react'; import { Calendar, Clock, ExternalLink, Globe, Star, Trophy, UserPlus } from 'lucide-react';
interface ProfileHeroProps { interface ProfileHeroProps {
@@ -93,7 +93,7 @@ export function ProfileHero({
<Stack direction="row" align="center" gap={3} wrap mb={2}> <Stack direction="row" align="center" gap={3} wrap mb={2}>
<Heading level={1}>{driver.name}</Heading> <Heading level={1}>{driver.name}</Heading>
<Text size="4xl" aria-label={`Country: ${driver.country}`}> <Text size="4xl" aria-label={`Country: ${driver.country}`}>
{CountryFlagDisplay.fromCountryCode(driver.country).toString()} {CountryFlagFormatter.fromCountryCode(driver.country).toString()}
</Text> </Text>
</Stack> </Stack>

View File

@@ -11,52 +11,41 @@ import { Input } from '@/ui/Input';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { import {
Activity, Activity,
AlertTriangle, AlertTriangle,
Bug, Bug,
ChevronDown, ChevronDown,
Clock, Clock,
Copy, Copy,
Cpu, Cpu,
Download, Download,
FileText, FileText,
Globe, Globe,
RefreshCw, RefreshCw,
Search, Search,
Terminal, Terminal,
Trash2, Trash2,
Zap Zap
} from 'lucide-react'; } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { DateFormatter } from '@/lib/formatters/DateFormatter';
import { DurationDisplay } from '@/lib/display-objects/DurationDisplay'; import { DurationFormatter } from '@/lib/formatters/DurationFormatter';
import { MemoryDisplay } from '@/lib/display-objects/MemoryDisplay'; import { MemoryFormatter } from '@/lib/formatters/MemoryFormatter';
import { PercentDisplay } from '@/lib/display-objects/PercentDisplay'; import { PercentFormatter } from '@/lib/formatters/PercentFormatter';
import { TimeDisplay } from '@/lib/display-objects/TimeDisplay';
interface ErrorAnalyticsDashboardProps {
/**
* Auto-refresh interval in milliseconds
*/
refreshInterval?: number;
/**
* Whether to show in production (default: false)
*/
showInProduction?: boolean;
}
function formatDuration(duration: number): string { function formatDuration(duration: number): string {
return DurationDisplay.formatMs(duration); return DurationFormatter.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 PercentDisplay.format(value / total); return PercentFormatter.format(value / total);
} }
function formatMemory(bytes: number): string { function formatMemory(bytes: number): string {
return MemoryDisplay.formatMB(bytes); return MemoryFormatter.formatMB(bytes);
} }
interface PerformanceWithMemory extends Performance { interface PerformanceWithMemory extends Performance {
@@ -327,7 +316,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">
{DateDisplay.formatTime(error.timestamp)} {DateFormatter.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

@@ -1,11 +1,11 @@
'use client'; 'use client';
import React, { useEffect, useState } from 'react'; import { TimeFormatter } from '@/lib/formatters/TimeFormatter';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { FeedItem } from '@/ui/FeedItem'; import { FeedItem } from '@/ui/FeedItem';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
import { TimeDisplay } from '@/lib/display-objects/TimeDisplay'; import { Text } from '@/ui/Text';
import { useEffect, useState } from 'react';
interface FeedItemData { interface FeedItemData {
id: string; id: string;
@@ -50,7 +50,7 @@ export function FeedItemCard({ item }: FeedItemCardProps) {
name: actor?.name || 'Unknown', name: actor?.name || 'Unknown',
avatar: actor?.avatarUrl avatar: actor?.avatarUrl
}} }}
timestamp={TimeDisplay.timeAgo(item.timestamp)} timestamp={TimeFormatter.timeAgo(item.timestamp)}
content={ content={
<Stack gap={2}> <Stack gap={2}>
<Text weight="bold" variant="high">{item.headline}</Text> <Text weight="bold" variant="high">{item.headline}</Text>

View File

@@ -1,14 +1,14 @@
import { FeedList } from '@/components/feed/FeedList'; import { FeedList } from '@/components/feed/FeedList';
import { LatestResultsSidebar } from '@/components/races/LatestResultsSidebar'; import { LatestResultsSidebar } from '@/components/races/LatestResultsSidebar';
import { UpcomingRacesSidebar } from '@/components/races/UpcomingRacesSidebar'; import { UpcomingRacesSidebar } from '@/components/races/UpcomingRacesSidebar';
import { DateFormatter } from '@/lib/formatters/DateFormatter';
import { Card } from '@/ui/Card'; import { Card } from '@/ui/Card';
import { Container } from '@/ui/Container'; import { Container } from '@/ui/Container';
import { Heading } from '@/ui/Heading';
import { Grid } from '@/ui/Grid'; import { Grid } from '@/ui/Grid';
import { Stack } from '@/ui/Stack'; import { Heading } from '@/ui/Heading';
import { Section } from '@/ui/Section'; import { Section } from '@/ui/Section';
import { Stack } from '@/ui/Stack';
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;
@@ -49,12 +49,12 @@ export function FeedLayout({
}: FeedLayoutProps) { }: FeedLayoutProps) {
const formattedUpcomingRaces = upcomingRaces.map(r => ({ const formattedUpcomingRaces = upcomingRaces.map(r => ({
...r, ...r,
formattedDate: DateDisplay.formatShort(r.scheduledAt), formattedDate: DateFormatter.formatShort(r.scheduledAt),
})); }));
const formattedLatestResults = latestResults.map(r => ({ const formattedLatestResults = latestResults.map(r => ({
...r, ...r,
formattedDate: DateDisplay.formatShort(r.scheduledAt), formattedDate: DateFormatter.formatShort(r.scheduledAt),
})); }));
return ( return (

View File

@@ -1,11 +1,11 @@
import { RankBadge } from '@/components/leaderboards/RankBadge'; import { RankBadge } from '@/components/leaderboards/RankBadge';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay'; import { RatingFormatter } from '@/lib/formatters/RatingFormatter';
import { SkillLevelDisplay } from '@/lib/display-objects/SkillLevelDisplay'; import { SkillLevelFormatter } from '@/lib/formatters/SkillLevelFormatter';
import { Avatar } from '@/ui/Avatar'; import { Avatar } from '@/ui/Avatar';
import { Group } from '@/ui/Group';
import { LeaderboardList } from '@/ui/LeaderboardList'; import { LeaderboardList } from '@/ui/LeaderboardList';
import { LeaderboardPreviewShell } from '@/ui/LeaderboardPreviewShell'; import { LeaderboardPreviewShell } from '@/ui/LeaderboardPreviewShell';
import { LeaderboardRow } from '@/ui/LeaderboardRow'; import { LeaderboardRow } from '@/ui/LeaderboardRow';
import { Group } from '@/ui/Group';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { Trophy } from 'lucide-react'; import { Trophy } from 'lucide-react';
@@ -69,8 +69,8 @@ export function DriverLeaderboardPreview({
</Text> </Text>
<Group gap={2}> <Group gap={2}>
<Text size="xs" variant="low" uppercase weight="bold" letterSpacing="wider">{driver.nationality}</Text> <Text size="xs" variant="low" uppercase weight="bold" letterSpacing="wider">{driver.nationality}</Text>
<Text size="xs" weight="bold" color={SkillLevelDisplay.getColor(driver.skillLevel)} uppercase letterSpacing="wider"> <Text size="xs" weight="bold" color={SkillLevelFormatter.getColor(driver.skillLevel)} uppercase letterSpacing="wider">
{SkillLevelDisplay.getLabel(driver.skillLevel)} {SkillLevelFormatter.getLabel(driver.skillLevel)}
</Text> </Text>
</Group> </Group>
</Group> </Group>
@@ -80,7 +80,7 @@ export function DriverLeaderboardPreview({
<Group gap={8}> <Group gap={8}>
<Group direction="column" align="end" gap={0}> <Group direction="column" align="end" gap={0}>
<Text variant="primary" font="mono" weight="bold" block size="md" align="right"> <Text variant="primary" font="mono" weight="bold" block size="md" align="right">
{RatingDisplay.format(driver.rating)} {RatingFormatter.format(driver.rating)}
</Text> </Text>
<Text size="xs" variant="low" block uppercase letterSpacing="widest" weight="bold" fontSize="9px" align="right"> <Text size="xs" variant="low" block uppercase letterSpacing="widest" weight="bold" fontSize="9px" align="right">
Rating Rating

View File

@@ -1,5 +1,5 @@
import { MedalDisplay } from '@/lib/display-objects/MedalDisplay'; import { MedalFormatter } from '@/lib/formatters/MedalFormatter';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay'; import { RatingFormatter } from '@/lib/formatters/RatingFormatter';
import { Image } from '@/ui/Image'; import { Image } from '@/ui/Image';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
@@ -91,8 +91,8 @@ export function LeaderboardPodium({ podium, onDriverClick }: LeaderboardPodiumPr
border border
transform="translateX(-50%)" transform="translateX(-50%)"
borderWidth="2px" borderWidth="2px"
bg={MedalDisplay.getBg(position)} bg={MedalFormatter.getBg(position)}
color={MedalDisplay.getColor(position)} color={MedalFormatter.getColor(position)}
shadow="lg" shadow="lg"
> >
<Text size="sm" weight="bold">{position}</Text> <Text size="sm" weight="bold">{position}</Text>
@@ -122,7 +122,7 @@ export function LeaderboardPodium({ podium, onDriverClick }: LeaderboardPodiumPr
block block
color={isFirst ? 'text-warning-amber' : 'text-primary-blue'} color={isFirst ? 'text-warning-amber' : 'text-primary-blue'}
> >
{RatingDisplay.format(driver.rating)} {RatingFormatter.format(driver.rating)}
</Text> </Text>
<Stack direction="row" align="center" gap={3} mt={1}> <Stack direction="row" align="center" gap={3} mt={1}>
@@ -155,7 +155,7 @@ export function LeaderboardPodium({ podium, onDriverClick }: LeaderboardPodiumPr
<Text <Text
weight="bold" weight="bold"
size="4xl" size="4xl"
color={MedalDisplay.getColor(position)} color={MedalFormatter.getColor(position)}
opacity={0.1} opacity={0.1}
fontSize={isFirst ? '5rem' : '3.5rem'} fontSize={isFirst ? '5rem' : '3.5rem'}
> >

View File

@@ -1,11 +1,10 @@
import { MedalDisplay } from '@/lib/display-objects/MedalDisplay'; import { MedalFormatter } from '@/lib/formatters/MedalFormatter';
import { RankMedal as UiRankMedal, RankMedalProps } from '@/ui/RankMedal'; import { RankMedalProps, RankMedal as UiRankMedal } from '@/ui/RankMedal';
import React from 'react';
export function RankMedal(props: RankMedalProps) { export function RankMedal(props: RankMedalProps) {
const variant = MedalDisplay.getVariant(props.rank); const variant = MedalFormatter.getVariant(props.rank);
const bg = MedalDisplay.getBg(props.rank); const bg = MedalFormatter.getBg(props.rank);
const color = MedalDisplay.getColor(props.rank); const color = MedalFormatter.getColor(props.rank);
return ( return (
<UiRankMedal <UiRankMedal

View File

@@ -1,12 +1,11 @@
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay'; import { RatingFormatter } from '@/lib/formatters/RatingFormatter';
import { SkillLevelFormatter } from '@/lib/formatters/SkillLevelFormatter';
import { Avatar } from '@/ui/Avatar'; import { Avatar } from '@/ui/Avatar';
import { Group } from '@/ui/Group'; import { Group } from '@/ui/Group';
import { LeaderboardRow } from '@/ui/LeaderboardRow';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { DeltaChip } from './DeltaChip'; import { DeltaChip } from './DeltaChip';
import { RankBadge } from './RankBadge'; import { RankBadge } from './RankBadge';
import { LeaderboardRow } from '@/ui/LeaderboardRow';
import { SkillLevelDisplay } from '@/lib/display-objects/SkillLevelDisplay';
import React from 'react';
interface RankingRowProps { interface RankingRowProps {
id: string; id: string;
@@ -65,8 +64,8 @@ export function RankingRow({
</Text> </Text>
<Group gap={2}> <Group gap={2}>
<Text size="xs" variant="low" uppercase weight="bold" letterSpacing="wider">{nationality}</Text> <Text size="xs" variant="low" uppercase weight="bold" letterSpacing="wider">{nationality}</Text>
<Text size="xs" weight="bold" style={{ color: SkillLevelDisplay.getColor(skillLevel) }} uppercase letterSpacing="wider"> <Text size="xs" weight="bold" style={{ color: SkillLevelFormatter.getColor(skillLevel) }} uppercase letterSpacing="wider">
{SkillLevelDisplay.getLabel(skillLevel)} {SkillLevelFormatter.getLabel(skillLevel)}
</Text> </Text>
</Group> </Group>
</Group> </Group>
@@ -84,7 +83,7 @@ export function RankingRow({
</Group> </Group>
<Group direction="column" align="end" gap={0}> <Group direction="column" align="end" gap={0}>
<Text variant="primary" font="mono" weight="bold" block size="md"> <Text variant="primary" font="mono" weight="bold" block size="md">
{RatingDisplay.format(rating)} {RatingFormatter.format(rating)}
</Text> </Text>
<Text size="xs" variant="low" block uppercase letterSpacing="widest" weight="bold"> <Text size="xs" variant="low" block uppercase letterSpacing="widest" weight="bold">
Rating Rating

View File

@@ -1,10 +1,7 @@
import { RatingFormatter } from '@/lib/formatters/RatingFormatter';
import { Avatar } from '@/ui/Avatar'; import { Avatar } from '@/ui/Avatar';
import { Group } from '@/ui/Group'; import { Group } from '@/ui/Group';
import { Text } from '@/ui/Text';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { MedalDisplay } from '@/lib/display-objects/MedalDisplay';
import { Surface } from '@/ui/Surface'; import { Surface } from '@/ui/Surface';
import React from 'react';
interface PodiumDriver { interface PodiumDriver {
id: string; id: string;
@@ -20,7 +17,7 @@ interface RankingsPodiumProps {
onDriverClick?: (id: string) => void; onDriverClick?: (id: string) => void;
} }
export function RankingsPodium({ podium, onDriverClick }: RankingsPodiumProps) { export function RankingsPodium({ podium }: RankingsPodiumProps) {
return ( return (
<Group justify="center" align="end" gap={4}> <Group justify="center" align="end" gap={4}>
{[1, 0, 2].map((index) => { {[1, 0, 2].map((index) => {
@@ -57,7 +54,7 @@ export function RankingsPodium({ podium, onDriverClick }: RankingsPodiumProps) {
<Text weight="bold" variant="high" size={isFirst ? 'md' : 'sm'}>{driver.name}</Text> <Text weight="bold" variant="high" size={isFirst ? 'md' : 'sm'}>{driver.name}</Text>
<Text font="mono" weight="bold" variant={isFirst ? 'warning' : 'primary'}> <Text font="mono" weight="bold" variant={isFirst ? 'warning' : 'primary'}>
{RatingDisplay.format(driver.rating)} {RatingFormatter.format(driver.rating)}
</Text> </Text>
</Group> </Group>

View File

@@ -1,18 +1,16 @@
'use client'; 'use client';
import React, { useState } from 'react'; import { DateFormatter } from '@/lib/formatters/DateFormatter';
import { useRouter } from 'next/navigation';
import { routes } from '@/lib/routing/RouteConfig';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Badge } from '@/ui/Badge'; import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Group } from '@/ui/Group'; import { Group } from '@/ui/Group';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface'; import { Surface } from '@/ui/Surface';
import { ChevronDown, ChevronUp, Calendar, CheckCircle, Trophy, Edit, Clock } from 'lucide-react'; import { Text } from '@/ui/Text';
import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { Calendar, CheckCircle, ChevronDown, ChevronUp, Clock, Edit, Trophy } from 'lucide-react';
import { useState } from 'react';
interface RaceEvent { interface RaceEvent {
id: string; id: string;
@@ -50,9 +48,6 @@ interface MonthGroup {
export function EnhancedLeagueSchedulePanel({ export function EnhancedLeagueSchedulePanel({
events, events,
leagueId,
currentDriverId,
isAdmin,
onRegister, onRegister,
onWithdraw, onWithdraw,
onEdit, onEdit,
@@ -60,7 +55,6 @@ export function EnhancedLeagueSchedulePanel({
onRaceDetail, onRaceDetail,
onResultsClick, onResultsClick,
}: EnhancedLeagueSchedulePanelProps) { }: EnhancedLeagueSchedulePanelProps) {
const router = useRouter();
const [expandedMonths, setExpandedMonths] = useState<Set<string>>(new Set()); const [expandedMonths, setExpandedMonths] = useState<Set<string>>(new Set());
// Group races by month // Group races by month
@@ -109,7 +103,7 @@ export function EnhancedLeagueSchedulePanel({
}; };
const formatTime = (scheduledAt: string) => { const formatTime = (scheduledAt: string) => {
return DateDisplay.formatDateTime(scheduledAt); return DateFormatter.formatDateTime(scheduledAt);
}; };
const groups = groupRacesByMonth(); const groups = groupRacesByMonth();
@@ -158,7 +152,7 @@ export function EnhancedLeagueSchedulePanel({
{isExpanded && ( {isExpanded && (
<Box p={4}> <Box p={4}>
<Stack gap={3}> <Stack gap={3}>
{group.races.map((race, raceIndex) => ( {group.races.map((race) => (
<Surface <Surface
key={race.id} key={race.id}
variant="precision" variant="precision"

View File

@@ -1,10 +1,10 @@
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 { RelativeTimeFormatter } from '@/lib/formatters/RelativeTimeFormatter';
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 { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { AlertTriangle, Calendar, Flag, Shield, UserMinus, UserPlus } from 'lucide-react'; import { AlertTriangle, Calendar, Flag, Shield, UserMinus, UserPlus } from 'lucide-react';
import { useMemo } from 'react'; import { useMemo } from 'react';
@@ -128,7 +128,7 @@ function ActivityItem({ activity }: { activity: LeagueActivity }) {
<ActivityFeedItem <ActivityFeedItem
icon={getIcon()} icon={getIcon()}
content={getContent()} content={getContent()}
timestamp={RelativeTimeDisplay.format(activity.timestamp, new Date())} timestamp={RelativeTimeFormatter.format(activity.timestamp, new Date())}
/> />
); );
} }

View File

@@ -1,11 +1,11 @@
import { DriverIdentity } from '@/ui/DriverIdentity'; import { DateFormatter } from '@/lib/formatters/DateFormatter';
import { DriverViewModel } from '@/lib/view-models/DriverViewModel'; import { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import { Badge } from '@/ui/Badge'; import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box'; import { Box } from '@/ui/Box';
import { DriverIdentity } from '@/ui/DriverIdentity';
import { TableCell, TableRow } from '@/ui/Table'; import { TableCell, TableRow } from '@/ui/Table';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { ReactNode } from 'react';
import React, { ReactNode } from 'react';
interface LeagueMemberRowProps { interface LeagueMemberRowProps {
driver?: DriverViewModel; driver?: DriverViewModel;
@@ -84,7 +84,7 @@ export function LeagueMemberRow({
</TableCell> </TableCell>
<TableCell> <TableCell>
<Text variant="high" size="sm"> <Text variant="high" size="sm">
{DateDisplay.formatShort(joinedAt)} {DateFormatter.formatShort(joinedAt)}
</Text> </Text>
</TableCell> </TableCell>
{actions && ( {actions && (

View File

@@ -1,39 +1,33 @@
'use client'; 'use client';
import {
Users,
Calendar,
Trophy,
Award,
Rocket,
Gamepad2,
User,
UsersRound,
Clock,
Flag,
Zap,
Timer,
Check,
Globe,
Medal,
type LucideIcon,
} from 'lucide-react';
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel'; import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
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 { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import {
Award,
Calendar,
Check,
Clock,
Flag,
Gamepad2,
Globe,
Medal,
Rocket,
Timer,
Trophy,
User,
Users,
UsersRound,
Zap,
type LucideIcon,
} from 'lucide-react';
import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { DateFormatter } from '@/lib/formatters/DateFormatter';
import { DurationDisplay } from '@/lib/display-objects/DurationDisplay';
interface LeagueReviewSummaryProps {
form: LeagueConfigFormModel;
presets: LeagueScoringPresetViewModel[];
}
// Individual review card component // Individual review card component
function ReviewCard({ function ReviewCard({
@@ -142,7 +136,7 @@ export function LeagueReviewSummary({ form, presets }: LeagueReviewSummaryProps)
const seasonStartLabel = const seasonStartLabel =
timings.seasonStartDate timings.seasonStartDate
? DateDisplay.formatShort(timings.seasonStartDate) ? DateFormatter.formatShort(timings.seasonStartDate)
: null; : null;
const stewardingLabel = (() => { const stewardingLabel = (() => {

View File

@@ -1,28 +1,27 @@
'use client'; 'use client';
import React from 'react'; import { DateFormatter } from '@/lib/formatters/DateFormatter';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Group } from '@/ui/Group';
import { Surface } from '@/ui/Surface';
import { Icon } from '@/ui/Icon';
import { Button } from '@/ui/Button';
import { Badge } from '@/ui/Badge'; import { Badge } from '@/ui/Badge';
import { import { Box } from '@/ui/Box';
Calendar, import { Button } from '@/ui/Button';
Clock, import { Group } from '@/ui/Group';
Car, import { Icon } from '@/ui/Icon';
MapPin, import { Stack } from '@/ui/Stack';
Thermometer, import { Surface } from '@/ui/Surface';
Droplets, import { Text } from '@/ui/Text';
Wind, import {
Calendar,
Car,
CheckCircle,
Clock,
Cloud, Cloud,
X, Droplets,
MapPin,
Thermometer,
Trophy, Trophy,
CheckCircle Wind,
X
} from 'lucide-react'; } from 'lucide-react';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
interface RaceDetailModalProps { interface RaceDetailModalProps {
race: { race: {
@@ -55,7 +54,7 @@ export function RaceDetailModal({
if (!isOpen) return null; if (!isOpen) return null;
const formatTime = (scheduledAt: string) => { const formatTime = (scheduledAt: string) => {
return DateDisplay.formatDateTime(scheduledAt); return DateFormatter.formatDateTime(scheduledAt);
}; };
const getStatusBadge = (status: 'scheduled' | 'completed') => { const getStatusBadge = (status: 'scheduled' | 'completed') => {

View File

@@ -1,15 +1,11 @@
'use client'; 'use client';
import { CountryFlagDisplay } from '@/lib/display-objects/CountryFlagDisplay'; import { CountryFlagFormatter } from '@/lib/formatters/CountryFlagFormatter';
import { Panel } from '@/ui/Panel';
import { Input } from '@/ui/Input';
import { Text } from '@/ui/Text';
import { TextArea } from '@/ui/TextArea';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group'; import { Group } from '@/ui/Group';
import { Input } from '@/ui/Input';
import { Panel } from '@/ui/Panel';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
import { ProfileStat } from '@/ui/ProfileHero'; import { TextArea } from '@/ui/TextArea';
import React from 'react';
interface ProfileDetailsPanelProps { interface ProfileDetailsPanelProps {
driver: { driver: {
@@ -50,7 +46,7 @@ export function ProfileDetailsPanel({ driver, isEditing, onUpdate }: ProfileDeta
<Text size="xs" variant="low" weight="bold" uppercase block>Nationality</Text> <Text size="xs" variant="low" weight="bold" uppercase block>Nationality</Text>
<Group gap={2}> <Group gap={2}>
<Text size="xl"> <Text size="xl">
{CountryFlagDisplay.fromCountryCode(driver.country).toString()} {CountryFlagFormatter.fromCountryCode(driver.country).toString()}
</Text> </Text>
<Text variant="med">{driver.country}</Text> <Text variant="med">{driver.country}</Text>
</Group> </Group>

View File

@@ -1,16 +1,16 @@
'use client'; 'use client';
import { mediaConfig } from '@/lib/config/mediaConfig'; import { mediaConfig } from '@/lib/config/mediaConfig';
import { CountryFlagDisplay } from '@/lib/display-objects/CountryFlagDisplay'; import { CountryFlagFormatter } from '@/lib/formatters/CountryFlagFormatter';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { Group } from '@/ui/Group';
import { Heading } from '@/ui/Heading'; import { Heading } from '@/ui/Heading';
import { Image } from '@/ui/Image'; import { Image } from '@/ui/Image';
import { ProfileHero, ProfileAvatar, ProfileStatsGroup, ProfileStat } from '@/ui/ProfileHero'; import { ProfileAvatar, ProfileHero, ProfileStat, ProfileStatsGroup } from '@/ui/ProfileHero';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
import { Calendar, Globe, Star, Trophy, UserPlus } from 'lucide-react'; import { Text } from '@/ui/Text';
import { Calendar, Globe, UserPlus } from 'lucide-react';
import React from 'react'; import React from 'react';
interface ProfileHeaderProps { interface ProfileHeaderProps {
@@ -56,7 +56,7 @@ export function ProfileHeader({
<Group gap={3}> <Group gap={3}>
<Heading level={1}>{driver.name}</Heading> <Heading level={1}>{driver.name}</Heading>
<Text size="2xl" aria-label={`Country: ${driver.country}`}> <Text size="2xl" aria-label={`Country: ${driver.country}`}>
{CountryFlagDisplay.fromCountryCode(driver.country).toString()} {CountryFlagFormatter.fromCountryCode(driver.country).toString()}
</Text> </Text>
</Group> </Group>
</Box> </Box>

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { DateFormatter } from '@/lib/formatters/DateFormatter';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { Card, Card as Surface } from '@/ui/Card'; import { Card, Card as Surface } from '@/ui/Card';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
@@ -64,7 +64,7 @@ export function SponsorshipRequestsPanel({
<Text size="xs" color="text-gray-400" block mt={1}>{request.message}</Text> <Text size="xs" color="text-gray-400" block mt={1}>{request.message}</Text>
)} )}
<Text size="xs" color="text-gray-500" block mt={2}> <Text size="xs" color="text-gray-500" block mt={2}>
{DateDisplay.formatShort(request.createdAtIso)} {DateFormatter.formatShort(request.createdAtIso)}
</Text> </Text>
</Stack> </Stack>
<Stack direction="row" gap={2}> <Stack direction="row" gap={2}>

View File

@@ -1,6 +1,5 @@
import React from 'react'; import { DateFormatter } from '@/lib/formatters/DateFormatter';
import { RaceStatusDisplay } from '@/lib/display-objects/RaceStatusDisplay'; import { RaceStatusFormatter } from '@/lib/formatters/RaceStatusFormatter';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import { RaceCard as UiRaceCard } from './RaceCard'; import { RaceCard as UiRaceCard } from './RaceCard';
interface RaceCardProps { interface RaceCardProps {
@@ -23,11 +22,11 @@ export function RaceCard({ race, onClick }: RaceCardProps) {
track={race.track} track={race.track}
car={race.car} car={race.car}
scheduledAt={race.scheduledAt} scheduledAt={race.scheduledAt}
scheduledAtLabel={DateDisplay.formatShort(race.scheduledAt)} scheduledAtLabel={DateFormatter.formatShort(race.scheduledAt)}
timeLabel={DateDisplay.formatTime(race.scheduledAt)} timeLabel={DateFormatter.formatTime(race.scheduledAt)}
status={race.status} status={race.status}
statusLabel={RaceStatusDisplay.getLabel(race.status)} statusLabel={RaceStatusFormatter.getLabel(race.status)}
statusVariant={RaceStatusDisplay.getVariant(race.status) as any} statusVariant={RaceStatusFormatter.getVariant(race.status) as any}
leagueName={race.leagueName} leagueName={race.leagueName}
leagueId={race.leagueId} leagueId={race.leagueId}
strengthOfField={race.strengthOfField} strengthOfField={race.strengthOfField}

View File

@@ -1,8 +1,8 @@
import { RaceHero as UiRaceHero } from '@/components/races/RaceHero'; import { RaceHero as UiRaceHero } from '@/components/races/RaceHero';
import { DateFormatter } from '@/lib/formatters/DateFormatter';
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;
@@ -34,8 +34,8 @@ export function RaceHero(props: RaceHeroProps) {
return ( return (
<UiRaceHero <UiRaceHero
{...rest} {...rest}
formattedDate={DateDisplay.formatShort(scheduledAt)} formattedDate={DateFormatter.formatShort(scheduledAt)}
formattedTime={DateDisplay.formatTime(scheduledAt)} formattedTime={DateFormatter.formatTime(scheduledAt)}
statusConfig={mappedConfig} statusConfig={mappedConfig}
/> />
); );

View File

@@ -1,9 +1,9 @@
import { routes } from '@/lib/routing/RouteConfig';
import { RaceListItem as UiRaceListItem } from '@/components/races/RaceListItem'; import { RaceListItem as UiRaceListItem } from '@/components/races/RaceListItem';
import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { DateFormatter } from '@/lib/formatters/DateFormatter';
import { StatusDisplay } from '@/lib/display-objects/StatusDisplay'; import { StatusFormatter } from '@/lib/formatters/StatusFormatter';
import { routes } from '@/lib/routing/RouteConfig';
interface Race { interface Race {
id: string; id: string;
@@ -48,11 +48,11 @@ export function RaceListItem({ race, onClick }: RaceListItemProps) {
<UiRaceListItem <UiRaceListItem
track={race.track} track={race.track}
car={race.car} car={race.car}
dateLabel={DateDisplay.formatMonthDay(race.scheduledAt).split(' ')[0]} dateLabel={DateFormatter.formatMonthDay(race.scheduledAt).split(' ')[0]}
dayLabel={DateDisplay.formatMonthDay(race.scheduledAt).split(' ')[1]} dayLabel={DateFormatter.formatMonthDay(race.scheduledAt).split(' ')[1]}
timeLabel={DateDisplay.formatTime(race.scheduledAt)} timeLabel={DateFormatter.formatTime(race.scheduledAt)}
status={race.status} status={race.status}
statusLabel={StatusDisplay.raceStatus(race.status)} statusLabel={StatusFormatter.raceStatus(race.status)}
statusVariant={config.variant} statusVariant={config.variant}
statusIconName={config.iconName} statusIconName={config.iconName}
leagueName={race.leagueName} leagueName={race.leagueName}

View File

@@ -1,8 +1,8 @@
import { DateFormatter } from '@/lib/formatters/DateFormatter';
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: {
@@ -29,7 +29,7 @@ export function RaceResultCard({
raceId={race.id} raceId={race.id}
track={race.track} track={race.track}
car={race.car} car={race.car}
formattedDate={DateDisplay.formatShort(race.scheduledAt)} formattedDate={DateFormatter.formatShort(race.scheduledAt)}
position={result.position} position={result.position}
positionLabel={result.formattedPosition} positionLabel={result.formattedPosition}
startPositionLabel={result.formattedStartPosition} startPositionLabel={result.formattedStartPosition}

View File

@@ -1,15 +1,14 @@
'use client'; 'use client';
import { CountryFlagDisplay } from '@/lib/display-objects/CountryFlagDisplay'; import { CountryFlagFormatter } from '@/lib/formatters/CountryFlagFormatter';
import { Image } from '@/ui/Image';
import { ResultRow, PositionBadge, ResultPoints } from '@/ui/ResultRow';
import { Text } from '@/ui/Text';
import { Badge } from '@/ui/Badge'; import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box'; import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group'; import { Group } from '@/ui/Group';
import { Image } from '@/ui/Image';
import { PositionBadge, ResultPoints, ResultRow } from '@/ui/ResultRow';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface'; import { Surface } from '@/ui/Surface';
import React from 'react'; import { Text } from '@/ui/Text';
interface ResultEntry { interface ResultEntry {
position: number; position: number;
@@ -62,7 +61,7 @@ export function RaceResultRow({ result, points }: RaceResultRowProps) {
justifyContent="center" justifyContent="center"
> >
<Text size="xs" style={{ fontSize: '0.625rem' }}> <Text size="xs" style={{ fontSize: '0.625rem' }}>
{CountryFlagDisplay.fromCountryCode(country).toString()} {CountryFlagFormatter.fromCountryCode(country).toString()}
</Text> </Text>
</Surface> </Surface>
</Box> </Box>

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import { mediaConfig } from '@/lib/config/mediaConfig'; import { mediaConfig } from '@/lib/config/mediaConfig';
import { CountryFlagDisplay } from '@/lib/display-objects/CountryFlagDisplay'; import { CountryFlagFormatter } from '@/lib/formatters/CountryFlagFormatter';
import { routes } from '@/lib/routing/RouteConfig'; import { routes } from '@/lib/routing/RouteConfig';
import { Card, Card as Surface } from '@/ui/Card'; import { Card, Card as Surface } from '@/ui/Card';
import { Heading } from '@/ui/Heading'; import { Heading } from '@/ui/Heading';
@@ -68,7 +68,7 @@ export function FriendsPreview({ friends }: FriendsPreviewProps) {
/> />
</Stack> </Stack>
<Text size="sm" color="text-white">{friend.name}</Text> <Text size="sm" color="text-white">{friend.name}</Text>
<Text size="lg">{CountryFlagDisplay.fromCountryCode(friend.country).toString()}</Text> <Text size="lg">{CountryFlagFormatter.fromCountryCode(friend.country).toString()}</Text>
</Surface> </Surface>
</Link> </Link>
</Stack> </Stack>

View File

@@ -1,6 +1,5 @@
'use client'; 'use client';
import { MinimalEmptyState } from '@/ui/EmptyState';
import { TeamRosterItem } from '@/components/teams/TeamRosterItem'; import { TeamRosterItem } from '@/components/teams/TeamRosterItem';
import { TeamRosterList } from '@/components/teams/TeamRosterList'; import { TeamRosterList } from '@/components/teams/TeamRosterList';
import { useTeamRoster } from "@/hooks/team/useTeamRoster"; import { useTeamRoster } from "@/hooks/team/useTeamRoster";
@@ -9,15 +8,16 @@ import { sortMembers } from '@/lib/utilities/roster-utils';
import type { DriverViewModel } from '@/lib/view-models/DriverViewModel'; import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card'; import { Card } from '@/ui/Card';
import { MinimalEmptyState } from '@/ui/EmptyState';
import { Heading } from '@/ui/Heading'; import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/Stack';
import { Select } from '@/ui/Select'; import { Select } from '@/ui/Select';
import { Stack } from '@/ui/Stack';
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 { DateFormatter } from '@/lib/formatters/DateFormatter';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay'; import { MemberFormatter } from '@/lib/formatters/MemberFormatter';
import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { RatingFormatter } from '@/lib/formatters/RatingFormatter';
export type TeamRole = 'owner' | 'admin' | 'member'; export type TeamRole = 'owner' | 'admin' | 'member';
export type TeamMemberRole = 'owner' | 'manager' | 'member'; export type TeamMemberRole = 'owner' | 'manager' | 'member';
@@ -74,7 +74,7 @@ export function TeamRoster({
const teamAverageRatingLabel = useMemo(() => { const teamAverageRatingLabel = useMemo(() => {
if (teamMembers.length === 0) return '—'; if (teamMembers.length === 0) return '—';
const avg = 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); return RatingFormatter.format(avg);
}, [teamMembers]); }, [teamMembers]);
if (loading) { if (loading) {
@@ -93,7 +93,7 @@ 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}>
{MemberDisplay.formatCount(memberships.length)} Avg Rating:{' '} {MemberFormatter.formatCount(memberships.length)} Avg Rating:{' '}
<Text color="text-primary-blue" weight="medium">{teamAverageRatingLabel}</Text> <Text color="text-primary-blue" weight="medium">{teamAverageRatingLabel}</Text>
</Text> </Text>
</Stack> </Stack>
@@ -129,8 +129,8 @@ 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)}
joinedAtLabel={DateDisplay.formatShort(joinedAt)} joinedAtLabel={DateFormatter.formatShort(joinedAt)}
ratingLabel={RatingDisplay.format(rating)} ratingLabel={RatingFormatter.format(rating)}
overallRankLabel={overallRank !== null ? `#${overallRank}` : null} overallRankLabel={overallRank !== null ? `#${overallRank}` : null}
actions={canManageMembership ? ( actions={canManageMembership ? (
<> <>

View File

@@ -1,10 +1,10 @@
import { useQuery } from '@tanstack/react-query';
import { useInject } from '@/lib/di/hooks/useInject'; import { useInject } from '@/lib/di/hooks/useInject';
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 { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
import { DateFormatter } from '@/lib/formatters/DateFormatter';
import type { RaceDTO } from '@/lib/types/generated/RaceDTO'; import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { LeagueScheduleRaceViewModel, LeagueScheduleViewModel } from '@/lib/view-models/LeagueScheduleViewModel';
import { useQuery } from '@tanstack/react-query';
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);
@@ -15,8 +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), formattedDate: DateFormatter.formatShort(scheduledAt),
formattedTime: DateDisplay.formatTime(scheduledAt), formattedTime: DateFormatter.formatTime(scheduledAt),
isPast, isPast,
isUpcoming: !isPast, isUpcoming: !isPast,
status: isPast ? 'completed' : 'scheduled', status: isPast ? 'completed' : 'scheduled',

View File

@@ -1,13 +1,13 @@
import { usePageData } from '@/lib/page/usePageData';
import { useInject } from '@/lib/di/hooks/useInject'; import { useInject } from '@/lib/di/hooks/useInject';
import { LEAGUE_SERVICE_TOKEN, LEAGUE_MEMBERSHIP_SERVICE_TOKEN } from '@/lib/di/tokens'; import { LEAGUE_MEMBERSHIP_SERVICE_TOKEN, LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
import { DateFormatter } from '@/lib/formatters/DateFormatter';
import { usePageData } from '@/lib/page/usePageData';
import type { LeagueSeasonSummaryDTO } from '@/lib/types/generated/LeagueSeasonSummaryDTO';
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility'; import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
import { LeagueAdminScheduleViewModel } from '@/lib/view-models/LeagueAdminScheduleViewModel'; import { LeagueAdminScheduleViewModel } from '@/lib/view-models/LeagueAdminScheduleViewModel';
import { LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueScheduleViewModel'; import { LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueScheduleViewModel';
import { LeagueSeasonSummaryViewModel } from '@/lib/view-models/LeagueSeasonSummaryViewModel'; import { LeagueSeasonSummaryViewModel } from '@/lib/view-models/LeagueSeasonSummaryViewModel';
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
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);
@@ -18,8 +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), formattedDate: DateFormatter.formatShort(scheduledAt),
formattedTime: DateDisplay.formatTime(scheduledAt), formattedTime: DateFormatter.formatTime(scheduledAt),
isPast, isPast,
isUpcoming: !isPast, isUpcoming: !isPast,
status: isPast ? 'completed' : 'scheduled', status: isPast ? 'completed' : 'scheduled',

View File

@@ -1,14 +1,14 @@
'use client'; 'use client';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { DashboardConsistencyFormatter } from '@/lib/formatters/DashboardConsistencyFormatter';
import { DashboardCountFormatter } from '@/lib/formatters/DashboardCountFormatter';
import { DashboardDateFormatter } from '@/lib/formatters/DashboardDateFormatter';
import { DashboardLeaguePositionFormatter } from '@/lib/formatters/DashboardLeaguePositionFormatter';
import { DashboardRankFormatter } from '@/lib/formatters/DashboardRankFormatter';
import { RatingFormatter } from '@/lib/formatters/RatingFormatter';
import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO'; import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
import type { DashboardViewData } from '@/lib/view-data/DashboardViewData'; import type { DashboardViewData } from '@/lib/view-data/DashboardViewData';
import { DashboardDateDisplay } from '@/lib/display-objects/DashboardDateDisplay';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { DashboardRankDisplay } from '@/lib/display-objects/DashboardRankDisplay';
import { DashboardConsistencyDisplay } from '@/lib/display-objects/DashboardConsistencyDisplay';
import { DashboardCountDisplay } from '@/lib/display-objects/DashboardCountDisplay';
import { DashboardLeaguePositionDisplay } from '@/lib/display-objects/DashboardLeaguePositionDisplay';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
export class DashboardViewDataBuilder { export class DashboardViewDataBuilder {
public static build(apiDto: DashboardOverviewDTO): DashboardViewData { public static build(apiDto: DashboardOverviewDTO): DashboardViewData {
@@ -17,21 +17,21 @@ export class DashboardViewDataBuilder {
name: apiDto.currentDriver?.name || '', name: apiDto.currentDriver?.name || '',
avatarUrl: apiDto.currentDriver?.avatarUrl || '', avatarUrl: apiDto.currentDriver?.avatarUrl || '',
country: apiDto.currentDriver?.country || '', country: apiDto.currentDriver?.country || '',
rating: apiDto.currentDriver ? RatingDisplay.format(apiDto.currentDriver.rating ?? 0) : '0.0', rating: apiDto.currentDriver ? RatingFormatter.format(apiDto.currentDriver.rating ?? 0) : '0.0',
rank: apiDto.currentDriver ? DashboardRankDisplay.format(apiDto.currentDriver.globalRank ?? 0) : '0', rank: apiDto.currentDriver ? DashboardRankFormatter.format(apiDto.currentDriver.globalRank ?? 0) : '0',
totalRaces: apiDto.currentDriver ? DashboardCountDisplay.format(apiDto.currentDriver.totalRaces ?? 0) : '0', totalRaces: apiDto.currentDriver ? DashboardCountFormatter.format(apiDto.currentDriver.totalRaces ?? 0) : '0',
wins: apiDto.currentDriver ? DashboardCountDisplay.format(apiDto.currentDriver.wins ?? 0) : '0', wins: apiDto.currentDriver ? DashboardCountFormatter.format(apiDto.currentDriver.wins ?? 0) : '0',
podiums: apiDto.currentDriver ? DashboardCountDisplay.format(apiDto.currentDriver.podiums ?? 0) : '0', podiums: apiDto.currentDriver ? DashboardCountFormatter.format(apiDto.currentDriver.podiums ?? 0) : '0',
consistency: apiDto.currentDriver ? DashboardConsistencyDisplay.format(apiDto.currentDriver.consistency ?? 0) : '0%', consistency: apiDto.currentDriver ? DashboardConsistencyFormatter.format(apiDto.currentDriver.consistency ?? 0) : '0%',
}, },
nextRace: apiDto.nextRace ? DashboardViewDataBuilder.buildNextRace(apiDto.nextRace) : null, nextRace: apiDto.nextRace ? DashboardViewDataBuilder.buildNextRace(apiDto.nextRace) : null,
upcomingRaces: apiDto.upcomingRaces.map((race) => DashboardViewDataBuilder.buildRace(race)), upcomingRaces: apiDto.upcomingRaces.map((race) => DashboardViewDataBuilder.buildRace(race)),
leagueStandings: apiDto.leagueStandingsSummaries.map((standing) => ({ leagueStandings: apiDto.leagueStandingsSummaries.map((standing) => ({
leagueId: standing.leagueId, leagueId: standing.leagueId,
leagueName: standing.leagueName, leagueName: standing.leagueName,
position: DashboardLeaguePositionDisplay.format(standing.position), position: DashboardLeaguePositionFormatter.format(standing.position),
points: DashboardCountDisplay.format(standing.points), points: DashboardCountFormatter.format(standing.points),
totalDrivers: DashboardCountDisplay.format(standing.totalDrivers), totalDrivers: DashboardCountFormatter.format(standing.totalDrivers),
})), })),
feedItems: apiDto.feedSummary.items.map((item) => ({ feedItems: apiDto.feedSummary.items.map((item) => ({
id: item.id, id: item.id,
@@ -39,7 +39,7 @@ export class DashboardViewDataBuilder {
headline: item.headline, headline: item.headline,
body: item.body ?? undefined, body: item.body ?? undefined,
timestamp: item.timestamp, timestamp: item.timestamp,
formattedTime: DashboardDateDisplay.format(new Date(item.timestamp)).relative, formattedTime: DashboardDateFormatter.format(new Date(item.timestamp)).relative,
ctaHref: item.ctaHref ?? undefined, ctaHref: item.ctaHref ?? undefined,
ctaLabel: item.ctaLabel ?? undefined, ctaLabel: item.ctaLabel ?? undefined,
})), })),
@@ -49,8 +49,8 @@ export class DashboardViewDataBuilder {
avatarUrl: friend.avatarUrl || '', avatarUrl: friend.avatarUrl || '',
country: friend.country, country: friend.country,
})), })),
activeLeaguesCount: DashboardCountDisplay.format(apiDto.activeLeaguesCount), activeLeaguesCount: DashboardCountFormatter.format(apiDto.activeLeaguesCount),
friendCount: DashboardCountDisplay.format(apiDto.friends.length), friendCount: DashboardCountFormatter.format(apiDto.friends.length),
hasUpcomingRaces: apiDto.upcomingRaces.length > 0, hasUpcomingRaces: apiDto.upcomingRaces.length > 0,
hasLeagueStandings: apiDto.leagueStandingsSummaries.length > 0, hasLeagueStandings: apiDto.leagueStandingsSummaries.length > 0,
hasFeedItems: apiDto.feedSummary.items.length > 0, hasFeedItems: apiDto.feedSummary.items.length > 0,
@@ -59,7 +59,7 @@ export class DashboardViewDataBuilder {
} }
private static buildNextRace(race: NonNullable<DashboardOverviewDTO['nextRace']>) { private static buildNextRace(race: NonNullable<DashboardOverviewDTO['nextRace']>) {
const dateInfo = DashboardDateDisplay.format(new Date(race.scheduledAt)); const dateInfo = DashboardDateFormatter.format(new Date(race.scheduledAt));
return { return {
id: race.id, id: race.id,
track: race.track, track: race.track,
@@ -73,7 +73,7 @@ export class DashboardViewDataBuilder {
} }
private static buildRace(race: DashboardOverviewDTO['upcomingRaces'][number]) { private static buildRace(race: DashboardOverviewDTO['upcomingRaces'][number]) {
const dateInfo = DashboardDateDisplay.format(new Date(race.scheduledAt)); const dateInfo = DashboardDateFormatter.format(new Date(race.scheduledAt));
return { return {
id: race.id, id: race.id,
track: race.track, track: race.track,

View File

@@ -1,13 +1,13 @@
'use client'; 'use client';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { DateFormatter } from '@/lib/formatters/DateFormatter';
import { FinishFormatter } from '@/lib/formatters/FinishFormatter';
import { NumberFormatter } from '@/lib/formatters/NumberFormatter';
import { PercentFormatter } from '@/lib/formatters/PercentFormatter';
import { RatingFormatter } from '@/lib/formatters/RatingFormatter';
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO'; import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
import type { DriverProfileViewData } from '@/lib/view-data/DriverProfileViewData'; import type { DriverProfileViewData } from '@/lib/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';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
export class DriverProfileViewDataBuilder { export class DriverProfileViewDataBuilder {
public static build(apiDto: GetDriverProfileOutputDTO): DriverProfileViewData { public static build(apiDto: GetDriverProfileOutputDTO): DriverProfileViewData {
@@ -19,9 +19,9 @@ 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), joinedAtLabel: DateFormatter.formatMonthYear(apiDto.currentDriver.joinedAt),
rating: apiDto.currentDriver.rating ?? null, rating: apiDto.currentDriver.rating ?? null,
ratingLabel: RatingDisplay.format(apiDto.currentDriver.rating), ratingLabel: RatingFormatter.format(apiDto.currentDriver.rating),
globalRank: apiDto.currentDriver.globalRank ?? null, globalRank: apiDto.currentDriver.globalRank ?? null,
globalRankLabel: apiDto.currentDriver.globalRank != null ? `#${apiDto.currentDriver.globalRank}` : '—', globalRankLabel: apiDto.currentDriver.globalRank != null ? `#${apiDto.currentDriver.globalRank}` : '—',
consistency: apiDto.currentDriver.consistency ?? null, consistency: apiDto.currentDriver.consistency ?? null,
@@ -30,27 +30,27 @@ export class DriverProfileViewDataBuilder {
} : null, } : null,
stats: apiDto.stats ? { stats: apiDto.stats ? {
totalRaces: apiDto.stats.totalRaces, totalRaces: apiDto.stats.totalRaces,
totalRacesLabel: NumberDisplay.format(apiDto.stats.totalRaces), totalRacesLabel: NumberFormatter.format(apiDto.stats.totalRaces),
wins: apiDto.stats.wins, wins: apiDto.stats.wins,
winsLabel: NumberDisplay.format(apiDto.stats.wins), winsLabel: NumberFormatter.format(apiDto.stats.wins),
podiums: apiDto.stats.podiums, podiums: apiDto.stats.podiums,
podiumsLabel: NumberDisplay.format(apiDto.stats.podiums), podiumsLabel: NumberFormatter.format(apiDto.stats.podiums),
dnfs: apiDto.stats.dnfs, dnfs: apiDto.stats.dnfs,
dnfsLabel: NumberDisplay.format(apiDto.stats.dnfs), dnfsLabel: NumberFormatter.format(apiDto.stats.dnfs),
avgFinish: apiDto.stats.avgFinish ?? null, avgFinish: apiDto.stats.avgFinish ?? null,
avgFinishLabel: FinishDisplay.formatAverage(apiDto.stats.avgFinish), avgFinishLabel: FinishFormatter.formatAverage(apiDto.stats.avgFinish),
bestFinish: apiDto.stats.bestFinish ?? null, bestFinish: apiDto.stats.bestFinish ?? null,
bestFinishLabel: FinishDisplay.format(apiDto.stats.bestFinish), bestFinishLabel: FinishFormatter.format(apiDto.stats.bestFinish),
worstFinish: apiDto.stats.worstFinish ?? null, worstFinish: apiDto.stats.worstFinish ?? null,
worstFinishLabel: FinishDisplay.format(apiDto.stats.worstFinish), worstFinishLabel: FinishFormatter.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), ratingLabel: RatingFormatter.format(apiDto.stats.rating),
consistency: apiDto.stats.consistency ?? null, consistency: apiDto.stats.consistency ?? null,
consistencyLabel: PercentDisplay.formatWhole(apiDto.stats.consistency), consistencyLabel: PercentFormatter.formatWhole(apiDto.stats.consistency),
overallRank: apiDto.stats.overallRank ?? null, overallRank: apiDto.stats.overallRank ?? null,
} : null, } : null,
finishDistribution: apiDto.finishDistribution ? { finishDistribution: apiDto.finishDistribution ? {
@@ -67,7 +67,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), joinedAtLabel: DateFormatter.formatMonthYear(m.joinedAt),
isCurrent: m.isCurrent, isCurrent: m.isCurrent,
})), })),
socialSummary: { socialSummary: {
@@ -93,7 +93,7 @@ export class DriverProfileViewDataBuilder {
rarity: a.rarity, rarity: a.rarity,
rarityLabel: a.rarity, rarityLabel: a.rarity,
earnedAt: a.earnedAt, earnedAt: a.earnedAt,
earnedAtLabel: DateDisplay.formatShort(a.earnedAt), earnedAtLabel: DateFormatter.formatShort(a.earnedAt),
})), })),
racingStyle: apiDto.extendedProfile.racingStyle, racingStyle: apiDto.extendedProfile.racingStyle,
favoriteTrack: apiDto.extendedProfile.favoriteTrack, favoriteTrack: apiDto.extendedProfile.favoriteTrack,

View File

@@ -1,10 +1,10 @@
'use client'; 'use client';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { MedalFormatter } from '@/lib/formatters/MedalFormatter';
import { WinRateFormatter } from '@/lib/formatters/WinRateFormatter';
import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO'; import type { DriverLeaderboardItemDTO } from '@/lib/types/generated/DriverLeaderboardItemDTO';
import type { DriverRankingsViewData } from '@/lib/view-data/DriverRankingsViewData'; import type { DriverRankingsViewData } from '@/lib/view-data/DriverRankingsViewData';
import { WinRateDisplay } from '@/lib/display-objects/WinRateDisplay';
import { MedalDisplay } from '@/lib/display-objects/MedalDisplay';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
export class DriverRankingsViewDataBuilder { export class DriverRankingsViewDataBuilder {
public static build(apiDto: DriverLeaderboardItemDTO[]): DriverRankingsViewData { public static build(apiDto: DriverLeaderboardItemDTO[]): DriverRankingsViewData {
@@ -31,9 +31,9 @@ export class DriverRankingsViewDataBuilder {
podiums: driver.podiums, podiums: driver.podiums,
rank: driver.rank, rank: driver.rank,
avatarUrl: driver.avatarUrl || '', avatarUrl: driver.avatarUrl || '',
winRate: WinRateDisplay.calculate(driver.racesCompleted, driver.wins), winRate: WinRateFormatter.calculate(driver.racesCompleted, driver.wins),
medalBg: MedalDisplay.getBg(driver.rank), medalBg: MedalFormatter.getBg(driver.rank),
medalColor: MedalDisplay.getColor(driver.rank), medalColor: MedalFormatter.getColor(driver.rank),
})), })),
podium: apiDto.slice(0, 3).map((driver, index) => { podium: apiDto.slice(0, 3).map((driver, index) => {
const positions = [2, 1, 3]; // Display order: 2nd, 1st, 3rd const positions = [2, 1, 3]; // Display order: 2nd, 1st, 3rd

View File

@@ -1,10 +1,10 @@
'use client'; 'use client';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { NumberFormatter } from '@/lib/formatters/NumberFormatter';
import { RatingFormatter } from '@/lib/formatters/RatingFormatter';
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO'; import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
import type { DriversViewData } from '@/lib/view-data/DriversViewData'; import type { DriversViewData } from '@/lib/view-data/DriversViewData';
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
export class DriversViewDataBuilder { export class DriversViewDataBuilder {
public static build(apiDto: DriversLeaderboardDTO): DriversViewData { public static build(apiDto: DriversLeaderboardDTO): DriversViewData {
@@ -13,7 +13,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), ratingLabel: RatingFormatter.format(driver.rating),
skillLevel: driver.skillLevel, skillLevel: driver.skillLevel,
category: driver.category ?? undefined, category: driver.category ?? undefined,
nationality: driver.nationality, nationality: driver.nationality,
@@ -25,12 +25,12 @@ export class DriversViewDataBuilder {
avatarUrl: driver.avatarUrl ?? undefined, avatarUrl: driver.avatarUrl ?? undefined,
})), })),
totalRaces: apiDto.totalRaces, totalRaces: apiDto.totalRaces,
totalRacesLabel: NumberDisplay.format(apiDto.totalRaces), totalRacesLabel: NumberFormatter.format(apiDto.totalRaces),
totalWins: apiDto.totalWins, totalWins: apiDto.totalWins,
totalWinsLabel: NumberDisplay.format(apiDto.totalWins), totalWinsLabel: NumberFormatter.format(apiDto.totalWins),
activeCount: apiDto.activeCount, activeCount: apiDto.activeCount,
activeCountLabel: NumberDisplay.format(apiDto.activeCount), activeCountLabel: NumberFormatter.format(apiDto.activeCount),
totalDriversLabel: NumberDisplay.format(apiDto.drivers.length), totalDriversLabel: NumberFormatter.format(apiDto.drivers.length),
}; };
} }
} }

View File

@@ -1,12 +1,12 @@
'use client'; 'use client';
import type { HealthDTO } from '@/lib/types/generated/HealthDTO';
import type { HealthViewData, HealthStatus, HealthMetrics, HealthComponent, HealthAlert } from '@/lib/view-data/HealthViewData';
import { HealthStatusDisplay } from '@/lib/display-objects/HealthStatusDisplay';
import { HealthMetricDisplay } from '@/lib/display-objects/HealthMetricDisplay';
import { HealthComponentDisplay } from '@/lib/display-objects/HealthComponentDisplay';
import { HealthAlertDisplay } from '@/lib/display-objects/HealthAlertDisplay';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder'; import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { HealthAlertFormatter } from '@/lib/formatters/HealthAlertFormatter';
import { HealthComponentFormatter } from '@/lib/formatters/HealthComponentFormatter';
import { HealthMetricFormatter } from '@/lib/formatters/HealthMetricFormatter';
import { HealthStatusFormatter } from '@/lib/formatters/HealthStatusFormatter';
import type { HealthDTO } from '@/lib/types/generated/HealthDTO';
import type { HealthAlert, HealthComponent, HealthMetrics, HealthStatus, HealthViewData } from '@/lib/view-data/HealthViewData';
export class HealthViewDataBuilder { export class HealthViewDataBuilder {
public static build(apiDto: HealthDTO): HealthViewData { public static build(apiDto: HealthDTO): HealthViewData {
@@ -17,37 +17,37 @@ export class HealthViewDataBuilder {
const overallStatus: HealthStatus = { const overallStatus: HealthStatus = {
status: apiDto.status, status: apiDto.status,
timestamp: apiDto.timestamp, timestamp: apiDto.timestamp,
formattedTimestamp: HealthStatusDisplay.formatTimestamp(apiDto.timestamp), formattedTimestamp: HealthStatusFormatter.formatTimestamp(apiDto.timestamp),
relativeTime: HealthStatusDisplay.formatRelativeTime(apiDto.timestamp), relativeTime: HealthStatusFormatter.formatRelativeTime(apiDto.timestamp),
statusLabel: HealthStatusDisplay.formatStatusLabel(apiDto.status), statusLabel: HealthStatusFormatter.formatStatusLabel(apiDto.status),
statusColor: HealthStatusDisplay.formatStatusColor(apiDto.status), statusColor: HealthStatusFormatter.formatStatusColor(apiDto.status),
statusIcon: HealthStatusDisplay.formatStatusIcon(apiDto.status), statusIcon: HealthStatusFormatter.formatStatusIcon(apiDto.status),
}; };
// Build metrics // Build metrics
const metrics: HealthMetrics = { const metrics: HealthMetrics = {
uptime: HealthMetricDisplay.formatUptime(apiDto.uptime), uptime: HealthMetricFormatter.formatUptime(apiDto.uptime),
responseTime: HealthMetricDisplay.formatResponseTime(apiDto.responseTime), responseTime: HealthMetricFormatter.formatResponseTime(apiDto.responseTime),
errorRate: HealthMetricDisplay.formatErrorRate(apiDto.errorRate), errorRate: HealthMetricFormatter.formatErrorRate(apiDto.errorRate),
lastCheck: apiDto.lastCheck || lastUpdated, lastCheck: apiDto.lastCheck || lastUpdated,
formattedLastCheck: HealthMetricDisplay.formatTimestamp(apiDto.lastCheck || lastUpdated), formattedLastCheck: HealthMetricFormatter.formatTimestamp(apiDto.lastCheck || lastUpdated),
checksPassed: apiDto.checksPassed || 0, checksPassed: apiDto.checksPassed || 0,
checksFailed: apiDto.checksFailed || 0, checksFailed: apiDto.checksFailed || 0,
totalChecks: (apiDto.checksPassed || 0) + (apiDto.checksFailed || 0), totalChecks: (apiDto.checksPassed || 0) + (apiDto.checksFailed || 0),
successRate: HealthMetricDisplay.formatSuccessRate(apiDto.checksPassed, apiDto.checksFailed), successRate: HealthMetricFormatter.formatSuccessRate(apiDto.checksPassed, apiDto.checksFailed),
}; };
// Build components // Build components
const components: HealthComponent[] = (apiDto.components || []).map((component) => ({ const components: HealthComponent[] = (apiDto.components || []).map((component) => ({
name: component.name, name: component.name,
status: component.status, status: component.status,
statusLabel: HealthComponentDisplay.formatStatusLabel(component.status), statusLabel: HealthComponentFormatter.formatStatusLabel(component.status),
statusColor: HealthComponentDisplay.formatStatusColor(component.status), statusColor: HealthComponentFormatter.formatStatusColor(component.status),
statusIcon: HealthComponentDisplay.formatStatusIcon(component.status), statusIcon: HealthComponentFormatter.formatStatusIcon(component.status),
lastCheck: component.lastCheck || lastUpdated, lastCheck: component.lastCheck || lastUpdated,
formattedLastCheck: HealthComponentDisplay.formatTimestamp(component.lastCheck || lastUpdated), formattedLastCheck: HealthComponentFormatter.formatTimestamp(component.lastCheck || lastUpdated),
responseTime: HealthMetricDisplay.formatResponseTime(component.responseTime), responseTime: HealthMetricFormatter.formatResponseTime(component.responseTime),
errorRate: HealthMetricDisplay.formatErrorRate(component.errorRate), errorRate: HealthMetricFormatter.formatErrorRate(component.errorRate),
})); }));
// Build alerts // Build alerts
@@ -57,10 +57,10 @@ export class HealthViewDataBuilder {
title: alert.title, title: alert.title,
message: alert.message, message: alert.message,
timestamp: alert.timestamp, timestamp: alert.timestamp,
formattedTimestamp: HealthAlertDisplay.formatTimestamp(alert.timestamp), formattedTimestamp: HealthAlertFormatter.formatTimestamp(alert.timestamp),
relativeTime: HealthAlertDisplay.formatRelativeTime(alert.timestamp), relativeTime: HealthAlertFormatter.formatRelativeTime(alert.timestamp),
severity: HealthAlertDisplay.formatSeverity(alert.type), severity: HealthAlertFormatter.formatSeverity(alert.type),
severityColor: HealthAlertDisplay.formatSeverityColor(alert.type), severityColor: HealthAlertFormatter.formatSeverityColor(alert.type),
})); }));
// Calculate derived fields // Calculate derived fields
@@ -77,7 +77,7 @@ export class HealthViewDataBuilder {
hasDegradedComponents, hasDegradedComponents,
hasErrorComponents, hasErrorComponents,
lastUpdated, lastUpdated,
formattedLastUpdated: HealthStatusDisplay.formatTimestamp(lastUpdated), formattedLastUpdated: HealthStatusFormatter.formatTimestamp(lastUpdated),
}; };
} }
} }

View File

@@ -1,9 +1,9 @@
'use client'; 'use client';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { DashboardDateFormatter } from '@/lib/formatters/DashboardDateFormatter';
import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO'; import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
import type { HomeViewData } from '@/lib/view-data/HomeViewData'; import type { HomeViewData } from '@/lib/view-data/HomeViewData';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { DashboardDateDisplay } from '@/lib/display-objects/DashboardDateDisplay';
export class HomeViewDataBuilder { export class HomeViewDataBuilder {
/** /**
@@ -19,7 +19,7 @@ export class HomeViewDataBuilder {
id: race.id, id: race.id,
track: race.track, track: race.track,
car: race.car, car: race.car,
formattedDate: DashboardDateDisplay.format(new Date(race.scheduledAt)).date, formattedDate: DashboardDateFormatter.format(new Date(race.scheduledAt)).date,
})), })),
topLeagues: (apiDto.leagueStandingsSummaries || []).map(league => ({ topLeagues: (apiDto.leagueStandingsSummaries || []).map(league => ({
id: league.leagueId, id: league.leagueId,

View File

@@ -1,8 +1,8 @@
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO';
import type { LeagueRosterAdminViewData, RosterMemberData, JoinRequestData } from '@/lib/view-data/LeagueRosterAdminViewData';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder'; import type { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';
import { DateFormatter } from '@/lib/formatters/DateFormatter';
import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO';
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
import type { JoinRequestData, LeagueRosterAdminViewData, RosterMemberData } from '@/lib/view-data/LeagueRosterAdminViewData';
type LeagueRosterAdminInputDTO = { type LeagueRosterAdminInputDTO = {
leagueId: string; leagueId: string;
@@ -23,7 +23,7 @@ export class LeagueRosterAdminViewDataBuilder {
}, },
role: member.role, role: member.role,
joinedAt: member.joinedAt, joinedAt: member.joinedAt,
formattedJoinedAt: DateDisplay.formatShort(member.joinedAt), formattedJoinedAt: DateFormatter.formatShort(member.joinedAt),
})); }));
// Transform join requests // Transform join requests
@@ -34,7 +34,7 @@ export class LeagueRosterAdminViewDataBuilder {
name: (req as { driver?: { name?: string } }).driver?.name || 'Unknown Driver', name: (req as { driver?: { name?: string } }).driver?.name || 'Unknown Driver',
}, },
requestedAt: req.requestedAt, requestedAt: req.requestedAt,
formattedRequestedAt: DateDisplay.formatShort(req.requestedAt), formattedRequestedAt: DateFormatter.formatShort(req.requestedAt),
message: req.message ?? undefined, message: req.message ?? undefined,
})); }));

View File

@@ -1,5 +1,5 @@
import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { DateFormatter } from '@/lib/formatters/DateFormatter';
import { StatusDisplay } from '@/lib/display-objects/StatusDisplay'; import { StatusFormatter } from '@/lib/formatters/StatusFormatter';
import { LeagueSponsorshipsApiDto } from '@/lib/types/tbd/LeagueSponsorshipsApiDto'; import { LeagueSponsorshipsApiDto } from '@/lib/types/tbd/LeagueSponsorshipsApiDto';
import { LeagueSponsorshipsViewData } from '@/lib/view-data/LeagueSponsorshipsViewData'; import { LeagueSponsorshipsViewData } from '@/lib/view-data/LeagueSponsorshipsViewData';
@@ -19,8 +19,8 @@ export class LeagueSponsorshipsViewDataBuilder implements ViewDataBuilder<any, a
sponsorshipSlots: apiDto.sponsorshipSlots, sponsorshipSlots: apiDto.sponsorshipSlots,
sponsorshipRequests: apiDto.sponsorshipRequests.map(r => ({ sponsorshipRequests: apiDto.sponsorshipRequests.map(r => ({
...r, ...r,
formattedRequestedAt: DateDisplay.formatShort(r.requestedAt), formattedRequestedAt: DateFormatter.formatShort(r.requestedAt),
statusLabel: StatusDisplay.protestStatus(r.status), // Reusing protest status for now statusLabel: StatusFormatter.protestStatus(r.status), // Reusing protest status for now
})), })),
}; };
} }

View File

@@ -1,12 +1,12 @@
import { mediaConfig } from '@/lib/config/mediaConfig';
import { CountryFlagFormatter } from '@/lib/formatters/CountryFlagFormatter';
import { DateFormatter } from '@/lib/formatters/DateFormatter';
import { FinishFormatter } from '@/lib/formatters/FinishFormatter';
import { NumberFormatter } from '@/lib/formatters/NumberFormatter';
import { PercentFormatter } from '@/lib/formatters/PercentFormatter';
import { RatingFormatter } from '@/lib/formatters/RatingFormatter';
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO'; import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
import type { ProfileViewData } from '@/lib/view-data/ProfileViewData'; import type { ProfileViewData } from '@/lib/view-data/ProfileViewData';
import { mediaConfig } from '@/lib/config/mediaConfig';
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';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
@@ -24,7 +24,7 @@ export class ProfileViewDataBuilder implements ViewDataBuilder<any, any> {
id: '', id: '',
name: '', name: '',
countryCode: '', countryCode: '',
countryFlag: CountryFlagDisplay.fromCountryCode(null).toString(), countryFlag: CountryFlagFormatter.fromCountryCode(null).toString(),
avatarUrl: mediaConfig.avatars.defaultFallback, avatarUrl: mediaConfig.avatars.defaultFallback,
bio: null, bio: null,
iracingId: null, iracingId: null,
@@ -45,25 +45,25 @@ export class ProfileViewDataBuilder implements ViewDataBuilder<any, any> {
id: driver.id, id: driver.id,
name: driver.name, name: driver.name,
countryCode: driver.country, countryCode: driver.country,
countryFlag: CountryFlagDisplay.fromCountryCode(driver.country).toString(), countryFlag: CountryFlagFormatter.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 ? String(driver.iracingId) : null, iracingId: driver.iracingId ? String(driver.iracingId) : null,
joinedAtLabel: DateDisplay.formatMonthYear(driver.joinedAt), joinedAtLabel: DateFormatter.formatMonthYear(driver.joinedAt),
}, },
stats: stats stats: stats
? { ? {
ratingLabel: RatingDisplay.format(stats.rating), ratingLabel: RatingFormatter.format(stats.rating),
globalRankLabel: driver.globalRank != null ? `#${driver.globalRank}` : '—', globalRankLabel: driver.globalRank != null ? `#${driver.globalRank}` : '—',
totalRacesLabel: NumberDisplay.format(stats.totalRaces), totalRacesLabel: NumberFormatter.format(stats.totalRaces),
winsLabel: NumberDisplay.format(stats.wins), winsLabel: NumberFormatter.format(stats.wins),
podiumsLabel: NumberDisplay.format(stats.podiums), podiumsLabel: NumberFormatter.format(stats.podiums),
dnfsLabel: NumberDisplay.format(stats.dnfs), dnfsLabel: NumberFormatter.format(stats.dnfs),
bestFinishLabel: FinishDisplay.format(stats.bestFinish), bestFinishLabel: FinishFormatter.format(stats.bestFinish),
worstFinishLabel: FinishDisplay.format(stats.worstFinish), worstFinishLabel: FinishFormatter.format(stats.worstFinish),
avgFinishLabel: FinishDisplay.formatAverage(stats.avgFinish), avgFinishLabel: FinishFormatter.formatAverage(stats.avgFinish),
consistencyLabel: PercentDisplay.formatWhole(stats.consistency), consistencyLabel: PercentFormatter.formatWhole(stats.consistency),
percentileLabel: PercentDisplay.format(stats.percentile), percentileLabel: PercentFormatter.format(stats.percentile),
} }
: null, : null,
teamMemberships: apiDto.teamMemberships.map((m) => ({ teamMemberships: apiDto.teamMemberships.map((m) => ({
@@ -71,7 +71,7 @@ export class ProfileViewDataBuilder implements ViewDataBuilder<any, any> {
teamName: m.teamName, teamName: m.teamName,
teamTag: m.teamTag || null, teamTag: m.teamTag || null,
roleLabel: m.role, roleLabel: m.role,
joinedAtLabel: DateDisplay.formatMonthYear(m.joinedAt), joinedAtLabel: DateFormatter.formatMonthYear(m.joinedAt),
href: `/teams/${m.teamId}`, href: `/teams/${m.teamId}`,
})), })),
extendedProfile: extended extendedProfile: extended
@@ -92,18 +92,18 @@ export class ProfileViewDataBuilder implements ViewDataBuilder<any, any> {
id: a.id, id: a.id,
title: a.title, title: a.title,
description: a.description, description: a.description,
earnedAtLabel: DateDisplay.formatShort(a.earnedAt), earnedAtLabel: DateFormatter.formatShort(a.earnedAt),
icon: a.icon as any, icon: a.icon as any,
rarityLabel: a.rarity, rarityLabel: a.rarity,
})), })),
friends: socialSummary.friends.slice(0, 8).map((f) => ({ friends: socialSummary.friends.slice(0, 8).map((f) => ({
id: f.id, id: f.id,
name: f.name, name: f.name,
countryFlag: CountryFlagDisplay.fromCountryCode(f.country).toString(), countryFlag: CountryFlagFormatter.fromCountryCode(f.country).toString(),
avatarUrl: f.avatarUrl || mediaConfig.avatars.defaultFallback, avatarUrl: f.avatarUrl || mediaConfig.avatars.defaultFallback,
href: `/drivers/${f.id}`, href: `/drivers/${f.id}`,
})), })),
friendsCountLabel: NumberDisplay.format(socialSummary.friendsCount), friendsCountLabel: NumberFormatter.format(socialSummary.friendsCount),
} }
: null, : null,
}; };

View File

@@ -1,8 +1,8 @@
import { DateFormatter } from '@/lib/formatters/DateFormatter';
import { RaceStatusFormatter } from '@/lib/formatters/RaceStatusFormatter';
import { RelativeTimeFormatter } from '@/lib/formatters/RelativeTimeFormatter';
import type { RacesPageDataDTO } from '@/lib/types/generated/RacesPageDataDTO'; import type { RacesPageDataDTO } from '@/lib/types/generated/RacesPageDataDTO';
import type { RacesViewData, RaceViewData } from '@/lib/view-data/RacesViewData'; import type { RacesViewData, RaceViewData } from '@/lib/view-data/RacesViewData';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import { RaceStatusDisplay } from '@/lib/display-objects/RaceStatusDisplay';
import { RelativeTimeDisplay } from '@/lib/display-objects/RelativeTimeDisplay';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
@@ -19,13 +19,13 @@ export class RacesViewDataBuilder implements ViewDataBuilder<any, any> {
track: race.track, track: race.track,
car: race.car, car: race.car,
scheduledAt: race.scheduledAt, scheduledAt: race.scheduledAt,
scheduledAtLabel: DateDisplay.formatShort(race.scheduledAt), scheduledAtLabel: DateFormatter.formatShort(race.scheduledAt),
timeLabel: DateDisplay.formatTime(race.scheduledAt), timeLabel: DateFormatter.formatTime(race.scheduledAt),
relativeTimeLabel: RelativeTimeDisplay.format(race.scheduledAt, now), relativeTimeLabel: RelativeTimeFormatter.format(race.scheduledAt, now),
status: race.status as RaceViewData['status'], status: race.status as RaceViewData['status'],
statusLabel: RaceStatusDisplay.getLabel(race.status), statusLabel: RaceStatusFormatter.getLabel(race.status),
statusVariant: RaceStatusDisplay.getVariant(race.status), statusVariant: RaceStatusFormatter.getVariant(race.status),
statusIconName: RaceStatusDisplay.getIconName(race.status), statusIconName: RaceStatusFormatter.getIconName(race.status),
sessionType: 'Race', sessionType: 'Race',
leagueId: race.leagueId, leagueId: race.leagueId,
leagueName: race.leagueName, leagueName: race.leagueName,

View File

@@ -1,9 +1,9 @@
import { DateFormatter } from '@/lib/formatters/DateFormatter';
import { LeagueFormatter } from '@/lib/formatters/LeagueFormatter';
import { MemberFormatter } from '@/lib/formatters/MemberFormatter';
import { NumberFormatter } from '@/lib/formatters/NumberFormatter';
import type { GetTeamDetailsOutputDTO } from '@/lib/types/generated/GetTeamDetailsOutputDTO'; import type { GetTeamDetailsOutputDTO } from '@/lib/types/generated/GetTeamDetailsOutputDTO';
import type { TeamDetailViewData, TeamDetailData, TeamMemberData, SponsorMetric, TeamTab } from '@/lib/view-data/TeamDetailViewData'; import type { SponsorMetric, TeamDetailData, TeamDetailViewData, TeamMemberData, TeamTab } from '@/lib/view-data/TeamDetailViewData';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import { MemberDisplay } from '@/lib/display-objects/MemberDisplay';
import { LeagueDisplay } from '@/lib/display-objects/LeagueDisplay';
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
@@ -21,7 +21,7 @@ export class TeamDetailViewDataBuilder implements ViewDataBuilder<any, any> {
ownerId: apiDto.team.ownerId, ownerId: apiDto.team.ownerId,
leagues: (apiDto.team as any).leagues || [], leagues: (apiDto.team as any).leagues || [],
createdAt: apiDto.team.createdAt, createdAt: apiDto.team.createdAt,
foundedDateLabel: apiDto.team.createdAt ? DateDisplay.formatMonthYear(apiDto.team.createdAt) : 'Unknown', foundedDateLabel: apiDto.team.createdAt ? DateFormatter.formatMonthYear(apiDto.team.createdAt) : 'Unknown',
specialization: (apiDto.team as any).specialization || null, specialization: (apiDto.team as any).specialization || null,
region: (apiDto.team as any).region || null, region: (apiDto.team as any).region || null,
languages: (apiDto.team as any).languages || [], languages: (apiDto.team as any).languages || [],
@@ -35,7 +35,7 @@ export class TeamDetailViewDataBuilder implements ViewDataBuilder<any, any> {
driverName: membership.driverName, driverName: membership.driverName,
role: membership.role, role: membership.role,
joinedAt: membership.joinedAt, joinedAt: membership.joinedAt,
joinedAtLabel: DateDisplay.formatShort(membership.joinedAt), joinedAtLabel: DateFormatter.formatShort(membership.joinedAt),
isActive: membership.isActive, isActive: membership.isActive,
avatarUrl: membership.avatarUrl, avatarUrl: membership.avatarUrl,
})); }));
@@ -51,19 +51,19 @@ export class TeamDetailViewDataBuilder implements ViewDataBuilder<any, any> {
{ {
icon: 'users', icon: 'users',
label: 'Members', label: 'Members',
value: NumberDisplay.format(memberships.length), value: NumberFormatter.format(memberships.length),
color: 'text-primary-blue', color: 'text-primary-blue',
}, },
{ {
icon: 'zap', icon: 'zap',
label: 'Est. Reach', label: 'Est. Reach',
value: NumberDisplay.format(memberships.length * 15), value: NumberFormatter.format(memberships.length * 15),
color: 'text-purple-400', color: 'text-purple-400',
}, },
{ {
icon: 'calendar', icon: 'calendar',
label: 'Races', label: 'Races',
value: NumberDisplay.format(leagueCount), value: NumberFormatter.format(leagueCount),
color: 'text-neon-aqua', color: 'text-neon-aqua',
}, },
{ {
@@ -89,8 +89,8 @@ export class TeamDetailViewDataBuilder implements ViewDataBuilder<any, any> {
isAdmin, isAdmin,
teamMetrics, teamMetrics,
tabs, tabs,
memberCountLabel: MemberDisplay.formatCount(memberships.length), memberCountLabel: MemberFormatter.formatCount(memberships.length),
leagueCountLabel: LeagueDisplay.formatCount(leagueCount), leagueCountLabel: LeagueFormatter.formatCount(leagueCount),
}; };
} }
} }

View File

@@ -1,8 +1,8 @@
import { NumberFormatter } from '@/lib/formatters/NumberFormatter';
import { RatingFormatter } from '@/lib/formatters/RatingFormatter';
import type { GetAllTeamsOutputDTO } from '@/lib/types/generated/GetAllTeamsOutputDTO'; import type { GetAllTeamsOutputDTO } from '@/lib/types/generated/GetAllTeamsOutputDTO';
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 type { TeamSummaryData, TeamsViewData } from '@/lib/view-data/TeamsViewData';
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder"; import { ViewDataBuilder } from "../../contracts/builders/ViewDataBuilder";
@@ -17,10 +17,10 @@ export class TeamsViewDataBuilder implements ViewDataBuilder<any, any> {
teamName: team.name, teamName: team.name,
memberCount: team.memberCount, memberCount: team.memberCount,
logoUrl: team.logoUrl || '', logoUrl: team.logoUrl || '',
ratingLabel: RatingDisplay.format(team.rating), ratingLabel: RatingFormatter.format(team.rating),
ratingValue: team.rating || 0, ratingValue: team.rating || 0,
winsLabel: NumberDisplay.format(team.totalWins || 0), winsLabel: NumberFormatter.format(team.totalWins || 0),
racesLabel: NumberDisplay.format(team.totalRaces || 0), racesLabel: NumberFormatter.format(team.totalRaces || 0),
region: team.region || '', region: team.region || '',
isRecruiting: team.isRecruiting, isRecruiting: team.isRecruiting,
category: team.category || '', category: team.category || '',

View File

@@ -1,5 +1,5 @@
import { LeagueWizardValidationMessages } from '@/lib/formatters/LeagueWizardValidationMessages';
import type { CreateLeagueInputDTO } from '@/lib/types/generated/CreateLeagueInputDTO'; import type { CreateLeagueInputDTO } from '@/lib/types/generated/CreateLeagueInputDTO';
import { LeagueWizardValidationMessages } from '@/lib/display-objects/LeagueWizardValidationMessages';
export type WizardStep = 1 | 2 | 3 | 4 | 5 | 6 | 7; export type WizardStep = 1 | 2 | 3 | 4 | 5 | 6 | 7;

View File

@@ -1,5 +1,5 @@
/** /**
* DisplayObject contract * Formatter contract
* *
* Deterministic, reusable, UI-only formatting/mapping logic. * Deterministic, reusable, UI-only formatting/mapping logic.
* *
@@ -12,18 +12,11 @@
* - No business rules * - No business rules
*/ */
export interface DisplayObject { export interface Formatter {
/** /**
* Format or map the display object * Format or map the display object
* *
* @returns Primitive values only (strings, numbers, booleans) * @returns Primitive values only (strings, numbers, booleans)
*/ */
format(): unknown; format(): unknown;
/**
* Optional: Get multiple display variants
*
* Allows a single DisplayObject to expose multiple presentation formats
*/
variants?(): Record<string, unknown>;
} }

View File

@@ -1,38 +0,0 @@
import { describe, it, expect } from 'vitest';
import { DashboardCountDisplay } from './DashboardCountDisplay';
describe('DashboardCountDisplay', () => {
describe('happy paths', () => {
it('should format positive numbers correctly', () => {
expect(DashboardCountDisplay.format(0)).toBe('0');
expect(DashboardCountDisplay.format(1)).toBe('1');
expect(DashboardCountDisplay.format(100)).toBe('100');
expect(DashboardCountDisplay.format(1000)).toBe('1000');
});
it('should handle null values', () => {
expect(DashboardCountDisplay.format(null)).toBe('0');
});
it('should handle undefined values', () => {
expect(DashboardCountDisplay.format(undefined)).toBe('0');
});
});
describe('edge cases', () => {
it('should handle negative numbers', () => {
expect(DashboardCountDisplay.format(-1)).toBe('-1');
expect(DashboardCountDisplay.format(-100)).toBe('-100');
});
it('should handle large numbers', () => {
expect(DashboardCountDisplay.format(999999)).toBe('999999');
expect(DashboardCountDisplay.format(1000000)).toBe('1000000');
});
it('should handle decimal numbers', () => {
expect(DashboardCountDisplay.format(1.5)).toBe('1.5');
expect(DashboardCountDisplay.format(100.99)).toBe('100.99');
});
});
});

View File

@@ -1,369 +0,0 @@
import { describe, it, expect } from 'vitest';
import { DashboardViewDataBuilder } from '../builders/view-data/DashboardViewDataBuilder';
import { DashboardDateDisplay } from './DashboardDateDisplay';
import { DashboardCountDisplay } from './DashboardCountDisplay';
import { DashboardRankDisplay } from './DashboardRankDisplay';
import { DashboardConsistencyDisplay } from './DashboardConsistencyDisplay';
import { DashboardLeaguePositionDisplay } from './DashboardLeaguePositionDisplay';
import { RatingDisplay } from './RatingDisplay';
import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
describe('Dashboard View Data - Cross-Component Consistency', () => {
describe('common patterns', () => {
it('should all use consistent formatting for numeric values', () => {
const dashboardDTO: DashboardOverviewDTO = {
currentDriver: {
id: 'driver-123',
name: 'John Doe',
country: 'USA',
rating: 1234.56,
globalRank: 42,
totalRaces: 150,
wins: 25,
podiums: 60,
consistency: 85,
},
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 3,
nextRace: null,
recentResults: [],
leagueStandingsSummaries: [
{
leagueId: 'league-1',
leagueName: 'Test League',
position: 5,
totalDrivers: 50,
points: 1250,
},
],
feedSummary: {
notificationCount: 0,
items: [],
},
friends: [
{ id: 'friend-1', name: 'Alice', country: 'UK' },
{ id: 'friend-2', name: 'Bob', country: 'Germany' },
],
};
const result = DashboardViewDataBuilder.build(dashboardDTO);
// All numeric values should be formatted as strings
expect(typeof result.currentDriver.rating).toBe('string');
expect(typeof result.currentDriver.rank).toBe('string');
expect(typeof result.currentDriver.totalRaces).toBe('string');
expect(typeof result.currentDriver.wins).toBe('string');
expect(typeof result.currentDriver.podiums).toBe('string');
expect(typeof result.currentDriver.consistency).toBe('string');
expect(typeof result.activeLeaguesCount).toBe('string');
expect(typeof result.friendCount).toBe('string');
expect(typeof result.leagueStandings[0].position).toBe('string');
expect(typeof result.leagueStandings[0].points).toBe('string');
expect(typeof result.leagueStandings[0].totalDrivers).toBe('string');
});
it('should all handle missing data gracefully', () => {
const dashboardDTO: DashboardOverviewDTO = {
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 0,
nextRace: null,
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: {
notificationCount: 0,
items: [],
},
friends: [],
};
const result = DashboardViewDataBuilder.build(dashboardDTO);
// All fields should have safe defaults
expect(result.currentDriver.name).toBe('');
expect(result.currentDriver.avatarUrl).toBe('');
expect(result.currentDriver.country).toBe('');
expect(result.currentDriver.rating).toBe('0.0');
expect(result.currentDriver.rank).toBe('0');
expect(result.currentDriver.totalRaces).toBe('0');
expect(result.currentDriver.wins).toBe('0');
expect(result.currentDriver.podiums).toBe('0');
expect(result.currentDriver.consistency).toBe('0%');
expect(result.nextRace).toBeNull();
expect(result.upcomingRaces).toEqual([]);
expect(result.leagueStandings).toEqual([]);
expect(result.feedItems).toEqual([]);
expect(result.friends).toEqual([]);
expect(result.activeLeaguesCount).toBe('0');
expect(result.friendCount).toBe('0');
});
it('should all preserve ISO timestamps for serialization', () => {
const now = new Date();
const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
const feedTimestamp = new Date(now.getTime() - 30 * 60 * 1000);
const dashboardDTO: DashboardOverviewDTO = {
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 1,
nextRace: {
id: 'race-1',
track: 'Spa',
car: 'Porsche',
scheduledAt: futureDate.toISOString(),
status: 'scheduled',
isMyLeague: true,
},
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: {
notificationCount: 1,
items: [
{
id: 'feed-1',
type: 'notification',
headline: 'Test',
timestamp: feedTimestamp.toISOString(),
},
],
},
friends: [],
};
const result = DashboardViewDataBuilder.build(dashboardDTO);
// All timestamps should be preserved as ISO strings
expect(result.nextRace?.scheduledAt).toBe(futureDate.toISOString());
expect(result.feedItems[0].timestamp).toBe(feedTimestamp.toISOString());
});
it('should all handle boolean flags correctly', () => {
const dashboardDTO: DashboardOverviewDTO = {
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [
{
id: 'race-1',
track: 'Spa',
car: 'Porsche',
scheduledAt: new Date().toISOString(),
status: 'scheduled',
isMyLeague: true,
},
{
id: 'race-2',
track: 'Monza',
car: 'Ferrari',
scheduledAt: new Date().toISOString(),
status: 'scheduled',
isMyLeague: false,
},
],
activeLeaguesCount: 1,
nextRace: null,
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: {
notificationCount: 0,
items: [],
},
friends: [],
};
const result = DashboardViewDataBuilder.build(dashboardDTO);
expect(result.upcomingRaces[0].isMyLeague).toBe(true);
expect(result.upcomingRaces[1].isMyLeague).toBe(false);
});
});
describe('data integrity', () => {
it('should maintain data consistency across transformations', () => {
const dashboardDTO: DashboardOverviewDTO = {
currentDriver: {
id: 'driver-123',
name: 'John Doe',
country: 'USA',
rating: 1234.56,
globalRank: 42,
totalRaces: 150,
wins: 25,
podiums: 60,
consistency: 85,
},
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 3,
nextRace: null,
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: {
notificationCount: 5,
items: [],
},
friends: [
{ id: 'friend-1', name: 'Alice', country: 'UK' },
{ id: 'friend-2', name: 'Bob', country: 'Germany' },
],
};
const result = DashboardViewDataBuilder.build(dashboardDTO);
// Verify derived fields match their source data
expect(result.friendCount).toBe(dashboardDTO.friends.length.toString());
expect(result.activeLeaguesCount).toBe(dashboardDTO.activeLeaguesCount.toString());
expect(result.hasFriends).toBe(dashboardDTO.friends.length > 0);
expect(result.hasUpcomingRaces).toBe(dashboardDTO.upcomingRaces.length > 0);
expect(result.hasLeagueStandings).toBe(dashboardDTO.leagueStandingsSummaries.length > 0);
expect(result.hasFeedItems).toBe(dashboardDTO.feedSummary.items.length > 0);
});
it('should handle complex real-world scenarios', () => {
const now = new Date();
const race1Date = new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000);
const race2Date = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000);
const feedTimestamp = new Date(now.getTime() - 60 * 60 * 1000);
const dashboardDTO: DashboardOverviewDTO = {
currentDriver: {
id: 'driver-123',
name: 'John Doe',
country: 'USA',
avatarUrl: 'https://example.com/avatar.jpg',
rating: 2456.78,
globalRank: 15,
totalRaces: 250,
wins: 45,
podiums: 120,
consistency: 92.5,
},
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [
{
id: 'race-1',
leagueId: 'league-1',
leagueName: 'Pro League',
track: 'Spa',
car: 'Porsche 911 GT3',
scheduledAt: race1Date.toISOString(),
status: 'scheduled',
isMyLeague: true,
},
{
id: 'race-2',
track: 'Monza',
car: 'Ferrari 488 GT3',
scheduledAt: race2Date.toISOString(),
status: 'scheduled',
isMyLeague: false,
},
],
activeLeaguesCount: 2,
nextRace: {
id: 'race-1',
leagueId: 'league-1',
leagueName: 'Pro League',
track: 'Spa',
car: 'Porsche 911 GT3',
scheduledAt: race1Date.toISOString(),
status: 'scheduled',
isMyLeague: true,
},
recentResults: [],
leagueStandingsSummaries: [
{
leagueId: 'league-1',
leagueName: 'Pro League',
position: 3,
totalDrivers: 100,
points: 2450,
},
{
leagueId: 'league-2',
leagueName: 'Rookie League',
position: 1,
totalDrivers: 50,
points: 1800,
},
],
feedSummary: {
notificationCount: 3,
items: [
{
id: 'feed-1',
type: 'race_result',
headline: 'Race completed',
body: 'You finished 3rd in the Pro League race',
timestamp: feedTimestamp.toISOString(),
ctaLabel: 'View Results',
ctaHref: '/races/123',
},
{
id: 'feed-2',
type: 'league_update',
headline: 'League standings updated',
body: 'You moved up 2 positions',
timestamp: feedTimestamp.toISOString(),
},
],
},
friends: [
{ id: 'friend-1', name: 'Alice', country: 'UK', avatarUrl: 'https://example.com/alice.jpg' },
{ id: 'friend-2', name: 'Bob', country: 'Germany' },
{ id: 'friend-3', name: 'Charlie', country: 'France', avatarUrl: 'https://example.com/charlie.jpg' },
],
};
const result = DashboardViewDataBuilder.build(dashboardDTO);
// Verify all transformations
expect(result.currentDriver.name).toBe('John Doe');
expect(result.currentDriver.rating).toBe('2,457');
expect(result.currentDriver.rank).toBe('15');
expect(result.currentDriver.totalRaces).toBe('250');
expect(result.currentDriver.wins).toBe('45');
expect(result.currentDriver.podiums).toBe('120');
expect(result.currentDriver.consistency).toBe('92.5%');
expect(result.nextRace).not.toBeNull();
expect(result.nextRace?.id).toBe('race-1');
expect(result.nextRace?.track).toBe('Spa');
expect(result.nextRace?.isMyLeague).toBe(true);
expect(result.upcomingRaces).toHaveLength(2);
expect(result.upcomingRaces[0].isMyLeague).toBe(true);
expect(result.upcomingRaces[1].isMyLeague).toBe(false);
expect(result.leagueStandings).toHaveLength(2);
expect(result.leagueStandings[0].position).toBe('#3');
expect(result.leagueStandings[0].points).toBe('2450');
expect(result.leagueStandings[1].position).toBe('#1');
expect(result.leagueStandings[1].points).toBe('1800');
expect(result.feedItems).toHaveLength(2);
expect(result.feedItems[0].type).toBe('race_result');
expect(result.feedItems[0].ctaLabel).toBe('View Results');
expect(result.feedItems[1].type).toBe('league_update');
expect(result.feedItems[1].ctaLabel).toBeUndefined();
expect(result.friends).toHaveLength(3);
expect(result.friends[0].avatarUrl).toBe('https://example.com/alice.jpg');
expect(result.friends[1].avatarUrl).toBe('');
expect(result.friends[2].avatarUrl).toBe('https://example.com/charlie.jpg');
expect(result.activeLeaguesCount).toBe('2');
expect(result.friendCount).toBe('3');
expect(result.hasUpcomingRaces).toBe(true);
expect(result.hasLeagueStandings).toBe(true);
expect(result.hasFeedItems).toBe(true);
expect(result.hasFriends).toBe(true);
});
});
});

View File

@@ -1,4 +1,4 @@
export class AchievementDisplay { export class AchievementFormatter {
static getRarityVariant(rarity: string) { static getRarityVariant(rarity: string) {
switch (rarity.toLowerCase()) { switch (rarity.toLowerCase()) {
case 'common': case 'common':

View File

@@ -4,7 +4,7 @@
* Deterministic mapping of engagement rates to activity level labels. * Deterministic mapping of engagement rates to activity level labels.
*/ */
export class ActivityLevelDisplay { export class ActivityLevelFormatter {
/** /**
* Maps engagement rate to activity level label. * Maps engagement rate to activity level label.
*/ */

View File

@@ -4,7 +4,7 @@
* Deterministic mapping of avatar-related data to display formats. * Deterministic mapping of avatar-related data to display formats.
*/ */
export class AvatarDisplay { export class AvatarFormatter {
/** /**
* Converts binary buffer to base64 string for display. * Converts binary buffer to base64 string for display.
*/ */

View File

@@ -1,18 +1,18 @@
export class CountryFlagDisplay { export class CountryFlagFormatter {
private constructor(private readonly value: string) {} private constructor(private readonly value: string) {}
static fromCountryCode(countryCode: string | null | undefined): CountryFlagDisplay { static fromCountryCode(countryCode: string | null | undefined): CountryFlagFormatter {
if (!countryCode) { if (!countryCode) {
return new CountryFlagDisplay('🏁'); return new CountryFlagFormatter('🏁');
} }
const code = countryCode.toUpperCase(); const code = countryCode.toUpperCase();
if (code.length !== 2) { if (code.length !== 2) {
return new CountryFlagDisplay('🏁'); return new CountryFlagFormatter('🏁');
} }
const codePoints = [...code].map((char) => 127397 + char.charCodeAt(0)); const codePoints = [...code].map((char) => 127397 + char.charCodeAt(0));
return new CountryFlagDisplay(String.fromCodePoint(...codePoints)); return new CountryFlagFormatter(String.fromCodePoint(...codePoints));
} }
toString(): string { toString(): string {

View File

@@ -5,7 +5,7 @@
* Avoids Intl and toLocaleString to prevent SSR/hydration mismatches. * Avoids Intl and toLocaleString to prevent SSR/hydration mismatches.
*/ */
export class CurrencyDisplay { export class CurrencyFormatter {
/** /**
* Formats an amount as currency (e.g., "$10.00" or "€1.000,00"). * Formats an amount as currency (e.g., "$10.00" or "€1.000,00").
* Default currency is USD. * Default currency is USD.

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'; import { describe, expect, it } from 'vitest';
import { DashboardConsistencyDisplay } from './DashboardConsistencyDisplay'; import { DashboardConsistencyDisplay } from './DashboardConsistencyFormatter';
describe('DashboardConsistencyDisplay', () => { describe('DashboardConsistencyDisplay', () => {
describe('happy paths', () => { describe('happy paths', () => {

View File

@@ -4,7 +4,7 @@
* Deterministic consistency formatting for dashboard display. * Deterministic consistency formatting for dashboard display.
*/ */
export class DashboardConsistencyDisplay { export class DashboardConsistencyFormatter {
static format(consistency: number): string { static format(consistency: number): string {
return `${consistency}%`; return `${consistency}%`;
} }

View File

@@ -0,0 +1,38 @@
import { describe, expect, it } from 'vitest';
import { DashboardCountFormatter } from './DashboardCountFormatter';
describe('DashboardCountDisplay', () => {
describe('happy paths', () => {
it('should format positive numbers correctly', () => {
expect(DashboardCountFormatter.format(0)).toBe('0');
expect(DashboardCountFormatter.format(1)).toBe('1');
expect(DashboardCountFormatter.format(100)).toBe('100');
expect(DashboardCountFormatter.format(1000)).toBe('1000');
});
it('should handle null values', () => {
expect(DashboardCountFormatter.format(null)).toBe('0');
});
it('should handle undefined values', () => {
expect(DashboardCountFormatter.format(undefined)).toBe('0');
});
});
describe('edge cases', () => {
it('should handle negative numbers', () => {
expect(DashboardCountFormatter.format(-1)).toBe('-1');
expect(DashboardCountFormatter.format(-100)).toBe('-100');
});
it('should handle large numbers', () => {
expect(DashboardCountFormatter.format(999999)).toBe('999999');
expect(DashboardCountFormatter.format(1000000)).toBe('1000000');
});
it('should handle decimal numbers', () => {
expect(DashboardCountFormatter.format(1.5)).toBe('1.5');
expect(DashboardCountFormatter.format(100.99)).toBe('100.99');
});
});
});

View File

@@ -4,7 +4,7 @@
* Deterministic count formatting for dashboard display. * Deterministic count formatting for dashboard display.
*/ */
export class DashboardCountDisplay { export class DashboardCountFormatter {
static format(count: number | null | undefined): string { static format(count: number | null | undefined): string {
if (count === null || count === undefined) { if (count === null || count === undefined) {
return '0'; return '0';

View File

@@ -14,7 +14,7 @@ export interface DashboardDateDisplayData {
/** /**
* Format date for display (deterministic, no Intl) * Format date for display (deterministic, no Intl)
*/ */
export class DashboardDateDisplay { export class DashboardDateFormatter {
static format(date: Date): DashboardDateDisplayData { static format(date: Date): DashboardDateDisplayData {
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'; import { describe, expect, it } from 'vitest';
import { DashboardLeaguePositionDisplay } from './DashboardLeaguePositionDisplay'; import { DashboardLeaguePositionDisplay } from './DashboardLeaguePositionFormatter';
describe('DashboardLeaguePositionDisplay', () => { describe('DashboardLeaguePositionDisplay', () => {
describe('happy paths', () => { describe('happy paths', () => {

View File

@@ -4,7 +4,7 @@
* Deterministic league position formatting for dashboard display. * Deterministic league position formatting for dashboard display.
*/ */
export class DashboardLeaguePositionDisplay { export class DashboardLeaguePositionFormatter {
static format(position: number | null | undefined): string { static format(position: number | null | undefined): string {
if (position === null || position === undefined) { if (position === null || position === undefined) {
return '-'; return '-';

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'; import { describe, expect, it } from 'vitest';
import { DashboardRankDisplay } from './DashboardRankDisplay'; import { DashboardRankDisplay } from './DashboardRankFormatter';
describe('DashboardRankDisplay', () => { describe('DashboardRankDisplay', () => {
describe('happy paths', () => { describe('happy paths', () => {

View File

@@ -4,7 +4,7 @@
* Deterministic rank formatting for dashboard display. * Deterministic rank formatting for dashboard display.
*/ */
export class DashboardRankDisplay { export class DashboardRankFormatter {
static format(rank: number): string { static format(rank: number): string {
return rank.toString(); return rank.toString();
} }

View File

@@ -1,4 +1,4 @@
export class DateDisplay { export class DateFormatter {
/** /**
* Formats a date as "Jan 18, 2026" using UTC. * Formats a date as "Jan 18, 2026" using UTC.
*/ */

View File

@@ -5,7 +5,7 @@
* to UI labels and variants. * to UI labels and variants.
*/ */
export class DriverRegistrationStatusDisplay { export class DriverRegistrationStatusFormatter {
static statusMessage(isRegistered: boolean): string { static statusMessage(isRegistered: boolean): string {
return isRegistered ? "Registered for this race" : "Not registered"; return isRegistered ? "Registered for this race" : "Not registered";
} }

View File

@@ -4,7 +4,7 @@
* Deterministic formatting for time durations. * Deterministic formatting for time durations.
*/ */
export class DurationDisplay { export class DurationFormatter {
/** /**
* Formats milliseconds as "123.45ms". * Formats milliseconds as "123.45ms".
*/ */

View File

@@ -4,7 +4,7 @@
* Deterministic formatting for race finish positions. * Deterministic formatting for race finish positions.
*/ */
export class FinishDisplay { export class FinishFormatter {
/** /**
* Formats a finish position as "P1", "P2", etc. * Formats a finish position as "P1", "P2", etc.
*/ */

View File

@@ -5,7 +5,7 @@
* This display object isolates UI-specific formatting from business logic. * This display object isolates UI-specific formatting from business logic.
*/ */
export class HealthAlertDisplay { export class HealthAlertFormatter {
static formatSeverity(type: 'critical' | 'warning' | 'info'): string { static formatSeverity(type: 'critical' | 'warning' | 'info'): string {
const severities: Record<string, string> = { const severities: Record<string, string> = {
critical: 'Critical', critical: 'Critical',

View File

@@ -5,7 +5,7 @@
* This display object isolates UI-specific formatting from business logic. * This display object isolates UI-specific formatting from business logic.
*/ */
export class HealthComponentDisplay { export class HealthComponentFormatter {
static formatStatusLabel(status: 'ok' | 'degraded' | 'error' | 'unknown'): string { static formatStatusLabel(status: 'ok' | 'degraded' | 'error' | 'unknown'): string {
const labels: Record<string, string> = { const labels: Record<string, string> = {
ok: 'Healthy', ok: 'Healthy',

View File

@@ -5,7 +5,7 @@
* This display object isolates UI-specific formatting from business logic. * This display object isolates UI-specific formatting from business logic.
*/ */
export class HealthMetricDisplay { export class HealthMetricFormatter {
static formatUptime(uptime?: number): string { static formatUptime(uptime?: number): string {
if (uptime === undefined || uptime === null) return 'N/A'; if (uptime === undefined || uptime === null) return 'N/A';
if (uptime < 0) return 'N/A'; if (uptime < 0) return 'N/A';

View File

@@ -5,7 +5,7 @@
* This display object isolates UI-specific formatting from business logic. * This display object isolates UI-specific formatting from business logic.
*/ */
export class HealthStatusDisplay { export class HealthStatusFormatter {
static formatStatusLabel(status: 'ok' | 'degraded' | 'error' | 'unknown'): string { static formatStatusLabel(status: 'ok' | 'degraded' | 'error' | 'unknown'): string {
const labels: Record<string, string> = { const labels: Record<string, string> = {
ok: 'Healthy', ok: 'Healthy',

View File

@@ -4,7 +4,7 @@
* Deterministic mapping of league creation status to display messages. * Deterministic mapping of league creation status to display messages.
*/ */
export class LeagueCreationStatusDisplay { export class LeagueCreationStatusFormatter {
/** /**
* Maps league creation success status to display message. * Maps league creation success status to display message.
*/ */

View File

@@ -4,7 +4,7 @@
* Deterministic display logic for leagues. * Deterministic display logic for leagues.
*/ */
export class LeagueDisplay { export class LeagueFormatter {
/** /**
* Formats a league count with pluralization. * Formats a league count with pluralization.
* Example: 1 -> "1 league", 2 -> "2 leagues" * Example: 1 -> "1 league", 2 -> "2 leagues"

View File

@@ -25,7 +25,7 @@ export const leagueRoleDisplay: Record<LeagueRole, LeagueRoleDisplayData> = {
} as const; } as const;
// For backward compatibility, also export the class with static method // For backward compatibility, also export the class with static method
export class LeagueRoleDisplay { export class LeagueRoleFormatter {
static getLeagueRoleDisplay(role: LeagueRole) { static getLeagueRoleDisplay(role: LeagueRole) {
return leagueRoleDisplay[role]; return leagueRoleDisplay[role];
} }

View File

@@ -11,7 +11,7 @@ export interface LeagueTierDisplayData {
icon: string; icon: string;
} }
export class LeagueTierDisplay { export class LeagueTierFormatter {
private static readonly CONFIG: Record<string, LeagueTierDisplayData> = { private static readonly CONFIG: Record<string, LeagueTierDisplayData> = {
premium: { premium: {
color: 'text-yellow-400', color: 'text-yellow-400',

View File

@@ -1,3 +1,4 @@
// TODO this file has no clear meaning
export class LeagueWizardValidationMessages { export class LeagueWizardValidationMessages {
static readonly LEAGUE_NAME_REQUIRED = 'League name is required'; static readonly LEAGUE_NAME_REQUIRED = 'League name is required';
static readonly LEAGUE_NAME_TOO_SHORT = 'League name must be at least 3 characters'; static readonly LEAGUE_NAME_TOO_SHORT = 'League name must be at least 3 characters';

View File

@@ -1,4 +1,4 @@
export class MedalDisplay { export class MedalFormatter {
static getVariant(position: number): 'warning' | 'low' | 'high' { static getVariant(position: number): 'warning' | 'low' | 'high' {
switch (position) { switch (position) {
case 1: return 'warning'; case 1: return 'warning';

View File

@@ -4,7 +4,7 @@
* Deterministic display logic for members. * Deterministic display logic for members.
*/ */
export class MemberDisplay { export class MemberFormatter {
/** /**
* Formats a member count with pluralization. * Formats a member count with pluralization.
* Example: 1 -> "1 member", 2 -> "2 members" * Example: 1 -> "1 member", 2 -> "2 members"

View File

@@ -1,4 +1,4 @@
export class MembershipFeeTypeDisplay { export class MembershipFeeTypeFormatter {
static format(type: string): string { static format(type: string): string {
switch (type) { switch (type) {
case 'season': return 'Per Season'; case 'season': return 'Per Season';

View File

@@ -4,7 +4,7 @@
* Deterministic formatting for memory usage. * Deterministic formatting for memory usage.
*/ */
export class MemoryDisplay { export class MemoryFormatter {
/** /**
* Formats bytes as "123.4MB". * Formats bytes as "123.4MB".
*/ */

View File

@@ -5,7 +5,7 @@
* Avoids Intl and toLocaleString to prevent SSR/hydration mismatches. * Avoids Intl and toLocaleString to prevent SSR/hydration mismatches.
*/ */
export class NumberDisplay { export class NumberFormatter {
/** /**
* Formats a number with thousands separators (commas). * Formats a number with thousands separators (commas).
* Example: 1234567 -> "1,234,567" * Example: 1234567 -> "1,234,567"

View File

@@ -4,7 +4,7 @@
* Deterministic mapping of onboarding status to display labels and variants. * Deterministic mapping of onboarding status to display labels and variants.
*/ */
export class OnboardingStatusDisplay { export class OnboardingStatusFormatter {
/** /**
* Maps onboarding success status to display label. * Maps onboarding success status to display label.
*/ */

View File

@@ -1,4 +1,4 @@
export class PayerTypeDisplay { export class PayerTypeFormatter {
static format(type: string): string { static format(type: string): string {
return type.charAt(0).toUpperCase() + type.slice(1); return type.charAt(0).toUpperCase() + type.slice(1);
} }

View File

@@ -1,4 +1,4 @@
export class PaymentTypeDisplay { export class PaymentTypeFormatter {
static format(type: string): string { static format(type: string): string {
return type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase()); return type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase());
} }

View File

@@ -4,7 +4,7 @@
* Deterministic formatting for percentages. * Deterministic formatting for percentages.
*/ */
export class PercentDisplay { export class PercentFormatter {
/** /**
* Formats a decimal value as a percentage string. * Formats a decimal value as a percentage string.
* Example: 0.1234 -> "12.3%" * Example: 0.1234 -> "12.3%"

View File

@@ -1,4 +1,4 @@
export class PrizeTypeDisplay { export class PrizeTypeFormatter {
static format(type: string): string { static format(type: string): string {
switch (type) { switch (type) {
case 'cash': return 'Cash Prize'; case 'cash': return 'Cash Prize';

View File

@@ -30,7 +30,7 @@ export interface TeamRoleDisplayData {
badgeClasses: string; badgeClasses: string;
} }
export class ProfileDisplay { export class ProfileFormatter {
private static readonly countryFlagDisplay: Record<string, CountryFlagDisplayData> = { private static readonly countryFlagDisplay: Record<string, CountryFlagDisplayData> = {
US: { flag: '🇺🇸', label: 'United States' }, US: { flag: '🇺🇸', label: 'United States' },
GB: { flag: '🇬🇧', label: 'United Kingdom' }, GB: { flag: '🇬🇧', label: 'United Kingdom' },
@@ -47,7 +47,7 @@ export class ProfileDisplay {
static getCountryFlag(countryCode: string): CountryFlagDisplayData { static getCountryFlag(countryCode: string): CountryFlagDisplayData {
const code = countryCode.toUpperCase(); const code = countryCode.toUpperCase();
return ProfileDisplay.countryFlagDisplay[code] || ProfileDisplay.countryFlagDisplay.DEFAULT; return ProfileFormatter.countryFlagDisplay[code] || ProfileFormatter.countryFlagDisplay.DEFAULT;
} }
private static readonly achievementRarityDisplay: Record<string, AchievementRarityDisplayData> = { private static readonly achievementRarityDisplay: Record<string, AchievementRarityDisplayData> = {
@@ -74,7 +74,7 @@ export class ProfileDisplay {
}; };
static getAchievementRarity(rarity: string): AchievementRarityDisplayData { static getAchievementRarity(rarity: string): AchievementRarityDisplayData {
return ProfileDisplay.achievementRarityDisplay[rarity] || ProfileDisplay.achievementRarityDisplay.common; return ProfileFormatter.achievementRarityDisplay[rarity] || ProfileFormatter.achievementRarityDisplay.common;
} }
private static readonly achievementIconDisplay: Record<string, AchievementIconDisplayData> = { private static readonly achievementIconDisplay: Record<string, AchievementIconDisplayData> = {
@@ -87,7 +87,7 @@ export class ProfileDisplay {
}; };
static getAchievementIcon(icon: string): AchievementIconDisplayData { static getAchievementIcon(icon: string): AchievementIconDisplayData {
return ProfileDisplay.achievementIconDisplay[icon] || ProfileDisplay.achievementIconDisplay.trophy; return ProfileFormatter.achievementIconDisplay[icon] || ProfileFormatter.achievementIconDisplay.trophy;
} }
private static readonly socialPlatformDisplay: Record<string, SocialPlatformDisplayData> = { private static readonly socialPlatformDisplay: Record<string, SocialPlatformDisplayData> = {
@@ -110,7 +110,7 @@ export class ProfileDisplay {
}; };
static getSocialPlatform(platform: string): SocialPlatformDisplayData { static getSocialPlatform(platform: string): SocialPlatformDisplayData {
return ProfileDisplay.socialPlatformDisplay[platform] || ProfileDisplay.socialPlatformDisplay.discord; return ProfileFormatter.socialPlatformDisplay[platform] || ProfileFormatter.socialPlatformDisplay.discord;
} }
static formatMonthYear(dateString: string): string { static formatMonthYear(dateString: string): string {
@@ -184,6 +184,6 @@ export class ProfileDisplay {
}; };
static getTeamRole(role: string): TeamRoleDisplayData { static getTeamRole(role: string): TeamRoleDisplayData {
return ProfileDisplay.teamRoleDisplay[role] || ProfileDisplay.teamRoleDisplay.member; return ProfileFormatter.teamRoleDisplay[role] || ProfileFormatter.teamRoleDisplay.member;
} }
} }

View File

@@ -6,7 +6,7 @@
export type RaceStatusVariant = 'info' | 'success' | 'neutral' | 'warning' | 'primary' | 'default'; export type RaceStatusVariant = 'info' | 'success' | 'neutral' | 'warning' | 'primary' | 'default';
export class RaceStatusDisplay { export class RaceStatusFormatter {
private static readonly CONFIG: Record<string, { variant: RaceStatusVariant; label: string; icon: string }> = { private static readonly CONFIG: Record<string, { variant: RaceStatusVariant; label: string; icon: string }> = {
scheduled: { scheduled: {
variant: 'primary', variant: 'primary',

View File

@@ -1,12 +1,12 @@
import { NumberDisplay } from './NumberDisplay'; import { NumberFormatter } from './NumberFormatter';
export class RatingDisplay { export class RatingFormatter {
/** /**
* Formats a rating as a rounded number with thousands separators. * Formats a rating as a rounded number with thousands separators.
* Example: 1234.56 -> "1,235" * Example: 1234.56 -> "1,235"
*/ */
static format(rating: number | null | undefined): string { static format(rating: number | null | undefined): string {
if (rating === null || rating === undefined) return '—'; if (rating === null || rating === undefined) return '—';
return NumberDisplay.format(Math.round(rating)); return NumberFormatter.format(Math.round(rating));
} }
} }

View File

@@ -1,4 +1,4 @@
export class RatingTrendDisplay { export class RatingTrendFormatter {
static getTrend(currentRating: number, previousRating: number | undefined): 'up' | 'down' | 'same' { static getTrend(currentRating: number, previousRating: number | undefined): 'up' | 'down' | 'same' {
if (!previousRating) return 'same'; if (!previousRating) return 'same';
if (currentRating > previousRating) return 'up'; if (currentRating > previousRating) return 'up';

View File

@@ -4,7 +4,7 @@
* Deterministic relative time formatting. * Deterministic relative time formatting.
*/ */
export class RelativeTimeDisplay { export class RelativeTimeFormatter {
/** /**
* Formats a date relative to "now". * Formats a date relative to "now".
* "now" must be passed as an argument for determinism. * "now" must be passed as an argument for determinism.

View File

@@ -10,7 +10,7 @@ export interface SeasonStatusDisplayData {
label: string; label: string;
} }
export class SeasonStatusDisplay { export class SeasonStatusFormatter {
private static readonly CONFIG: Record<string, SeasonStatusDisplayData> = { private static readonly CONFIG: Record<string, SeasonStatusDisplayData> = {
active: { active: {
color: 'text-performance-green', color: 'text-performance-green',

View File

@@ -1,4 +1,4 @@
export class SkillLevelDisplay { export class SkillLevelFormatter {
static getLabel(skillLevel: string): string { static getLabel(skillLevel: string): string {
const levels: Record<string, string> = { const levels: Record<string, string> = {
pro: 'Pro', pro: 'Pro',

View File

@@ -1,4 +1,4 @@
export class SkillLevelIconDisplay { export class SkillLevelIconFormatter {
static getIcon(skillLevel: string): string { static getIcon(skillLevel: string): string {
const icons: Record<string, string> = { const icons: Record<string, string> = {
beginner: '🥉', beginner: '🥉',

View File

@@ -4,7 +4,7 @@
* Deterministic mapping of status codes to human-readable labels. * Deterministic mapping of status codes to human-readable labels.
*/ */
export class StatusDisplay { export class StatusFormatter {
/** /**
* Maps transaction status to label. * Maps transaction status to label.
*/ */

View File

@@ -4,7 +4,7 @@
* Deterministic mapping of team creation status to display messages. * Deterministic mapping of team creation status to display messages.
*/ */
export class TeamCreationStatusDisplay { export class TeamCreationStatusFormatter {
/** /**
* Maps team creation success status to display message. * Maps team creation success status to display message.
*/ */

View File

@@ -1,4 +1,4 @@
export class TimeDisplay { export class TimeFormatter {
static timeAgo(timestamp: Date | string): string { static timeAgo(timestamp: Date | string): string {
const date = typeof timestamp === 'string' ? new Date(timestamp) : timestamp; const date = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;
const diffMs = Date.now() - date.getTime(); const diffMs = Date.now() - date.getTime();

View File

@@ -1,4 +1,4 @@
export class TransactionTypeDisplay { export class TransactionTypeFormatter {
static format(type: string): string { static format(type: string): string {
return type.charAt(0).toUpperCase() + type.slice(1); return type.charAt(0).toUpperCase() + type.slice(1);
} }

Some files were not shown because too many files have changed in this diff Show More