do to formatters
This commit is contained in:
@@ -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 (
|
||||||
|
|||||||
@@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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())}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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 = (() => {
|
||||||
|
|||||||
@@ -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') => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 || '',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
|
||||||
}
|
}
|
||||||
@@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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':
|
||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
@@ -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 {
|
||||||
@@ -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.
|
||||||
@@ -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', () => {
|
||||||
@@ -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}%`;
|
||||||
}
|
}
|
||||||
38
apps/website/lib/formatters/DashboardCountFormatter.test.ts
Normal file
38
apps/website/lib/formatters/DashboardCountFormatter.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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';
|
||||||
@@ -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'];
|
||||||
@@ -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', () => {
|
||||||
@@ -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 '-';
|
||||||
@@ -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', () => {
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
@@ -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";
|
||||||
}
|
}
|
||||||
@@ -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".
|
||||||
*/
|
*/
|
||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
@@ -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',
|
||||||
@@ -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',
|
||||||
@@ -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';
|
||||||
@@ -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',
|
||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
@@ -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"
|
||||||
@@ -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];
|
||||||
}
|
}
|
||||||
@@ -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',
|
||||||
@@ -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';
|
||||||
@@ -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';
|
||||||
@@ -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"
|
||||||
@@ -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';
|
||||||
@@ -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".
|
||||||
*/
|
*/
|
||||||
@@ -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"
|
||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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());
|
||||||
}
|
}
|
||||||
@@ -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%"
|
||||||
@@ -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';
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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',
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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';
|
||||||
@@ -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.
|
||||||
@@ -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',
|
||||||
@@ -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',
|
||||||
@@ -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: '🥉',
|
||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
@@ -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();
|
||||||
@@ -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
Reference in New Issue
Block a user