website refactor
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -9,7 +9,8 @@ interface DriverHeaderPanelProps {
|
||||
avatarUrl?: string;
|
||||
nationality: string;
|
||||
rating: number;
|
||||
globalRank?: number | null;
|
||||
ratingLabel: string;
|
||||
globalRankLabel?: string | null;
|
||||
bio?: string | null;
|
||||
actions?: React.ReactNode;
|
||||
}
|
||||
@@ -19,7 +20,8 @@ export function DriverHeaderPanel({
|
||||
avatarUrl,
|
||||
nationality,
|
||||
rating,
|
||||
globalRank,
|
||||
ratingLabel,
|
||||
globalRankLabel,
|
||||
bio,
|
||||
actions
|
||||
}: DriverHeaderPanelProps) {
|
||||
@@ -54,8 +56,8 @@ export function DriverHeaderPanel({
|
||||
rounded="2xl"
|
||||
overflow="hidden"
|
||||
border
|
||||
borderColor="border-charcoal-outline"
|
||||
bg="bg-graphite-black"
|
||||
borderColor="border-charcoal-outline"
|
||||
bg="bg-graphite-black"
|
||||
flexShrink={0}
|
||||
>
|
||||
<Image
|
||||
@@ -73,16 +75,16 @@ export function DriverHeaderPanel({
|
||||
<Text as="h1" size="3xl" weight="bold" color="text-white">
|
||||
{name}
|
||||
</Text>
|
||||
<RatingBadge rating={rating} size="lg" />
|
||||
<RatingBadge rating={rating} ratingLabel={ratingLabel} size="lg" />
|
||||
</Stack>
|
||||
|
||||
<Stack direction="row" align="center" gap={4} wrap>
|
||||
<Text size="sm" color="text-gray-400">
|
||||
{nationality}
|
||||
</Text>
|
||||
{globalRank !== undefined && globalRank !== null && (
|
||||
{globalRankLabel && (
|
||||
<Text size="sm" color="text-gray-400">
|
||||
Global Rank: <Text color="text-warning-amber" weight="semibold">#{globalRank}</Text>
|
||||
Global Rank: <Text color="text-warning-amber" weight="semibold">{globalRankLabel}</Text>
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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())}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 = (() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -9,21 +9,21 @@ import { ArrowDownLeft, ArrowUpRight, History, Wallet } from 'lucide-react';
|
||||
|
||||
interface Transaction {
|
||||
id: string;
|
||||
type: 'credit' | 'debit';
|
||||
amount: number;
|
||||
type: 'deposit' | 'withdrawal' | 'sponsorship' | 'prize';
|
||||
formattedAmount: string;
|
||||
description: string;
|
||||
date: string;
|
||||
formattedDate: string;
|
||||
}
|
||||
|
||||
interface WalletSummaryPanelProps {
|
||||
balance: number;
|
||||
formattedBalance: string;
|
||||
currency: string;
|
||||
transactions: Transaction[];
|
||||
onDeposit: () => void;
|
||||
onWithdraw: () => void;
|
||||
}
|
||||
|
||||
export function WalletSummaryPanel({ balance, currency, transactions, onDeposit, onWithdraw }: WalletSummaryPanelProps) {
|
||||
export function WalletSummaryPanel({ formattedBalance, currency, transactions, onDeposit, onWithdraw }: WalletSummaryPanelProps) {
|
||||
return (
|
||||
<Stack gap={6}>
|
||||
<Surface variant="dark" border rounded="lg" padding={8} position="relative" overflow="hidden">
|
||||
@@ -48,7 +48,7 @@ export function WalletSummaryPanel({ balance, currency, transactions, onDeposit,
|
||||
</Stack>
|
||||
<Stack direction="row" align="baseline" gap={2}>
|
||||
<Text size="4xl" weight="bold" color="text-white">
|
||||
{balance.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
{formattedBalance}
|
||||
</Text>
|
||||
<Text size="xl" weight="medium" color="text-gray-500">{currency}</Text>
|
||||
</Stack>
|
||||
@@ -87,37 +87,40 @@ export function WalletSummaryPanel({ balance, currency, transactions, onDeposit,
|
||||
<Text color="text-gray-500">No recent transactions.</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
transactions.map((tx) => (
|
||||
<Stack key={tx.id} p={4} borderBottom borderColor="border-charcoal-outline" hoverBg="bg-white/5" transition>
|
||||
<Stack direction="row" justify="between" align="center">
|
||||
<Stack direction="row" align="center" gap={4}>
|
||||
<Stack
|
||||
center
|
||||
w={10}
|
||||
h={10}
|
||||
rounded="full"
|
||||
bg={tx.type === 'credit' ? 'bg-performance-green/10' : 'bg-error-red/10'}
|
||||
transactions.map((tx) => {
|
||||
const isCredit = tx.type === 'deposit' || tx.type === 'sponsorship';
|
||||
return (
|
||||
<Stack key={tx.id} p={4} borderBottom borderColor="border-charcoal-outline" hoverBg="bg-white/5" transition>
|
||||
<Stack direction="row" justify="between" align="center">
|
||||
<Stack direction="row" align="center" gap={4}>
|
||||
<Stack
|
||||
center
|
||||
w={10}
|
||||
h={10}
|
||||
rounded="full"
|
||||
bg={isCredit ? 'bg-performance-green/10' : 'bg-error-red/10'}
|
||||
>
|
||||
<Icon
|
||||
icon={isCredit ? ArrowDownLeft : ArrowUpRight}
|
||||
size={4}
|
||||
color={isCredit ? 'text-performance-green' : 'text-error-red'}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack gap={0.5}>
|
||||
<Text weight="medium" color="text-white">{tx.description}</Text>
|
||||
<Text size="xs" color="text-gray-500">{tx.formattedDate}</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Text
|
||||
weight="bold"
|
||||
color={isCredit ? 'text-performance-green' : 'text-white'}
|
||||
>
|
||||
<Icon
|
||||
icon={tx.type === 'credit' ? ArrowDownLeft : ArrowUpRight}
|
||||
size={4}
|
||||
color={tx.type === 'credit' ? 'text-performance-green' : 'text-error-red'}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack gap={0.5}>
|
||||
<Text weight="medium" color="text-white">{tx.description}</Text>
|
||||
<Text size="xs" color="text-gray-500">{new Date(tx.date).toLocaleDateString()}</Text>
|
||||
</Stack>
|
||||
{tx.formattedAmount}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Text
|
||||
weight="bold"
|
||||
color={tx.type === 'credit' ? 'text-performance-green' : 'text-white'}
|
||||
>
|
||||
{tx.type === 'credit' ? '+' : '-'}{tx.amount.toFixed(2)}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
))
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Stack>
|
||||
</Surface>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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)" />
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' }} />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
@@ -88,4 +108,4 @@ export class DriverProfileViewDataBuilder {
|
||||
} : null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
|
||||
@@ -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
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 };
|
||||
|
||||
24
apps/website/lib/display-objects/DurationDisplay.ts
Normal file
24
apps/website/lib/display-objects/DurationDisplay.ts
Normal 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')}`;
|
||||
}
|
||||
}
|
||||
24
apps/website/lib/display-objects/FinishDisplay.ts
Normal file
24
apps/website/lib/display-objects/FinishDisplay.ts
Normal 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)}`;
|
||||
}
|
||||
}
|
||||
21
apps/website/lib/display-objects/MemoryDisplay.ts
Normal file
21
apps/website/lib/display-objects/MemoryDisplay.ts
Normal 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`;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
25
apps/website/lib/display-objects/PercentDisplay.ts
Normal file
25
apps/website/lib/display-objects/PercentDisplay.ts
Normal 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)}%`;
|
||||
}
|
||||
}
|
||||
44
apps/website/lib/display-objects/StatusDisplay.ts
Normal file
44
apps/website/lib/display-objects/StatusDisplay.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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)}%`;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ export interface TeamSummaryData {
|
||||
leagueName: string;
|
||||
memberCount: number;
|
||||
logoUrl?: string;
|
||||
ratingLabel: string;
|
||||
winsLabel: string;
|
||||
racesLabel: string;
|
||||
}
|
||||
|
||||
export interface TeamsViewData extends ViewData {
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface LeagueSponsorshipsViewData {
|
||||
sponsorId: string;
|
||||
sponsorName: string;
|
||||
requestedAt: string;
|
||||
formattedRequestedAt: string;
|
||||
status: 'pending' | 'approved' | 'rejected';
|
||||
statusLabel: string;
|
||||
}>;
|
||||
}
|
||||
@@ -7,6 +7,8 @@ export interface LeagueScheduleRaceViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
scheduledAt: Date;
|
||||
formattedDate: string;
|
||||
formattedTime: string;
|
||||
isPast: boolean;
|
||||
isUpcoming: boolean;
|
||||
status: string;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -121,7 +121,7 @@ export function LeagueSponsorshipsTemplate({ viewData }: LeagueSponsorshipsTempl
|
||||
key={request.id}
|
||||
request={{
|
||||
...request,
|
||||
slotName: slot?.name || 'Unknown slot'
|
||||
slotName: slot?.name || 'Unknown slot',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>"{req.message}"</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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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'.
|
||||
|
||||
Reference in New Issue
Block a user