website refactor

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

View File

@@ -1,11 +1,15 @@
'use client';
import { useSponsorSponsorships } from "@/hooks/sponsor/useSponsorSponsorships";
import { SponsorCampaignsTemplate, SponsorshipType } from "@/templates/SponsorCampaignsTemplate";
import { SponsorCampaignsTemplate, SponsorshipType, SponsorCampaignsViewData } from "@/templates/SponsorCampaignsTemplate";
import { Box } from "@/ui/Box";
import { 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');
@@ -39,22 +43,33 @@ export default function SponsorCampaignsPage() {
}
// 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,
totalInvestment: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'active').reduce((sum: number, s: any) => sum + s.price, 0),
totalImpressions: sponsorshipsData.sponsorships.reduce((sum: number, s: any) => sum + s.impressions, 0),
formattedTotalInvestment: CurrencyDisplay.format(totalInvestment),
formattedTotalImpressions: NumberDisplay.formatCompact(totalImpressions),
};
const viewData = {
sponsorships: sponsorshipsData.sponsorships as any,
stats,
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 = sponsorshipsData.sponsorships.filter((s: 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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