website refactor
This commit is contained in:
@@ -1,11 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useSponsorSponsorships } from "@/hooks/sponsor/useSponsorSponsorships";
|
import { useSponsorSponsorships } from "@/hooks/sponsor/useSponsorSponsorships";
|
||||||
import { SponsorCampaignsTemplate, SponsorshipType } from "@/templates/SponsorCampaignsTemplate";
|
import { SponsorCampaignsTemplate, SponsorshipType, SponsorCampaignsViewData } from "@/templates/SponsorCampaignsTemplate";
|
||||||
import { Box } from "@/ui/Box";
|
import { Box } from "@/ui/Box";
|
||||||
import { Button } from "@/ui/Button";
|
import { Button } from "@/ui/Button";
|
||||||
import { Text } from "@/ui/Text";
|
import { Text } from "@/ui/Text";
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { CurrencyDisplay } from "@/lib/display-objects/CurrencyDisplay";
|
||||||
|
import { NumberDisplay } from "@/lib/display-objects/NumberDisplay";
|
||||||
|
import { DateDisplay } from "@/lib/display-objects/DateDisplay";
|
||||||
|
import { StatusDisplay } from "@/lib/display-objects/StatusDisplay";
|
||||||
|
|
||||||
export default function SponsorCampaignsPage() {
|
export default function SponsorCampaignsPage() {
|
||||||
const [typeFilter, setTypeFilter] = useState<SponsorshipType>('all');
|
const [typeFilter, setTypeFilter] = useState<SponsorshipType>('all');
|
||||||
@@ -39,22 +43,33 @@ export default function SponsorCampaignsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate stats
|
// Calculate stats
|
||||||
|
const totalInvestment = sponsorshipsData.sponsorships.filter((s: any) => s.status === 'active').reduce((sum: number, s: any) => sum + s.price, 0);
|
||||||
|
const totalImpressions = sponsorshipsData.sponsorships.reduce((sum: number, s: any) => sum + s.impressions, 0);
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
total: sponsorshipsData.sponsorships.length,
|
total: sponsorshipsData.sponsorships.length,
|
||||||
active: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'active').length,
|
active: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'active').length,
|
||||||
pending: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'pending_approval').length,
|
pending: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'pending_approval').length,
|
||||||
approved: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'approved').length,
|
approved: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'approved').length,
|
||||||
rejected: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'rejected').length,
|
rejected: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'rejected').length,
|
||||||
totalInvestment: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'active').reduce((sum: number, s: any) => sum + s.price, 0),
|
formattedTotalInvestment: CurrencyDisplay.format(totalInvestment),
|
||||||
totalImpressions: sponsorshipsData.sponsorships.reduce((sum: number, s: any) => sum + s.impressions, 0),
|
formattedTotalImpressions: NumberDisplay.formatCompact(totalImpressions),
|
||||||
};
|
};
|
||||||
|
|
||||||
const viewData = {
|
const sponsorships = sponsorshipsData.sponsorships.map((s: any) => ({
|
||||||
sponsorships: sponsorshipsData.sponsorships as any,
|
...s,
|
||||||
stats,
|
formattedInvestment: CurrencyDisplay.format(s.price),
|
||||||
|
formattedImpressions: NumberDisplay.format(s.impressions),
|
||||||
|
formattedStartDate: s.seasonStartDate ? DateDisplay.formatShort(s.seasonStartDate) : undefined,
|
||||||
|
formattedEndDate: s.seasonEndDate ? DateDisplay.formatShort(s.seasonEndDate) : undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const viewData: SponsorCampaignsViewData = {
|
||||||
|
sponsorships,
|
||||||
|
stats: stats as any,
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredSponsorships = sponsorshipsData.sponsorships.filter((s: any) => {
|
const filteredSponsorships = sponsorships.filter((s: any) => {
|
||||||
// For now, we only have leagues in the DTO
|
// For now, we only have leagues in the DTO
|
||||||
if (typeFilter !== 'all' && typeFilter !== 'leagues') return false;
|
if (typeFilter !== 'all' && typeFilter !== 'leagues') return false;
|
||||||
if (searchQuery && !s.leagueName.toLowerCase().includes(searchQuery.toLowerCase())) return false;
|
if (searchQuery && !s.leagueName.toLowerCase().includes(searchQuery.toLowerCase())) return false;
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRos
|
|||||||
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
|
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
|
||||||
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
|
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
|
||||||
|
|
||||||
|
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
||||||
|
|
||||||
const ROLE_OPTIONS: MembershipRole[] = ['owner', 'admin', 'steward', 'member'];
|
const ROLE_OPTIONS: MembershipRole[] = ['owner', 'admin', 'steward', 'member'];
|
||||||
|
|
||||||
export function RosterAdminPage({ viewData: initialViewData }: Partial<ClientWrapperProps<LeagueRosterAdminViewData>>) {
|
export function RosterAdminPage({ viewData: initialViewData }: Partial<ClientWrapperProps<LeagueRosterAdminViewData>>) {
|
||||||
@@ -81,6 +83,7 @@ export function RosterAdminPage({ viewData: initialViewData }: Partial<ClientWra
|
|||||||
id: req.id,
|
id: req.id,
|
||||||
driver: req.driver as { id: string; name: string },
|
driver: req.driver as { id: string; name: string },
|
||||||
requestedAt: req.requestedAt,
|
requestedAt: req.requestedAt,
|
||||||
|
formattedRequestedAt: DateDisplay.formatShort(req.requestedAt),
|
||||||
message: req.message || undefined,
|
message: req.message || undefined,
|
||||||
})),
|
})),
|
||||||
members: members.map((m: LeagueRosterMemberDTO): RosterMemberData => ({
|
members: members.map((m: LeagueRosterMemberDTO): RosterMemberData => ({
|
||||||
@@ -88,6 +91,7 @@ export function RosterAdminPage({ viewData: initialViewData }: Partial<ClientWra
|
|||||||
driver: m.driver as { id: string; name: string },
|
driver: m.driver as { id: string; name: string },
|
||||||
role: m.role,
|
role: m.role,
|
||||||
joinedAt: m.joinedAt,
|
joinedAt: m.joinedAt,
|
||||||
|
formattedJoinedAt: DateDisplay.formatShort(m.joinedAt),
|
||||||
})),
|
})),
|
||||||
}), [leagueId, joinRequests, members]);
|
}), [leagueId, joinRequests, members]);
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ interface Achievement {
|
|||||||
description: string;
|
description: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
rarity: 'common' | 'rare' | 'epic' | 'legendary' | string;
|
rarity: 'common' | 'rare' | 'epic' | 'legendary' | string;
|
||||||
earnedAt: Date;
|
earnedAtLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AchievementGridProps {
|
interface AchievementGridProps {
|
||||||
@@ -72,7 +72,7 @@ export function AchievementGrid({ achievements }: AchievementGridProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text size="xs" variant="low">•</Text>
|
<Text size="xs" variant="low">•</Text>
|
||||||
<Text size="xs" variant="low">
|
<Text size="xs" variant="low">
|
||||||
{AchievementDisplay.formatDate(achievement.earnedAt)}
|
{achievement.earnedAtLabel}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ interface DriverHeaderPanelProps {
|
|||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
nationality: string;
|
nationality: string;
|
||||||
rating: number;
|
rating: number;
|
||||||
globalRank?: number | null;
|
ratingLabel: string;
|
||||||
|
globalRankLabel?: string | null;
|
||||||
bio?: string | null;
|
bio?: string | null;
|
||||||
actions?: React.ReactNode;
|
actions?: React.ReactNode;
|
||||||
}
|
}
|
||||||
@@ -19,7 +20,8 @@ export function DriverHeaderPanel({
|
|||||||
avatarUrl,
|
avatarUrl,
|
||||||
nationality,
|
nationality,
|
||||||
rating,
|
rating,
|
||||||
globalRank,
|
ratingLabel,
|
||||||
|
globalRankLabel,
|
||||||
bio,
|
bio,
|
||||||
actions
|
actions
|
||||||
}: DriverHeaderPanelProps) {
|
}: DriverHeaderPanelProps) {
|
||||||
@@ -54,8 +56,8 @@ export function DriverHeaderPanel({
|
|||||||
rounded="2xl"
|
rounded="2xl"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
border
|
border
|
||||||
borderColor="border-charcoal-outline"
|
borderColor="border-charcoal-outline"
|
||||||
bg="bg-graphite-black"
|
bg="bg-graphite-black"
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
@@ -73,16 +75,16 @@ export function DriverHeaderPanel({
|
|||||||
<Text as="h1" size="3xl" weight="bold" color="text-white">
|
<Text as="h1" size="3xl" weight="bold" color="text-white">
|
||||||
{name}
|
{name}
|
||||||
</Text>
|
</Text>
|
||||||
<RatingBadge rating={rating} size="lg" />
|
<RatingBadge rating={rating} ratingLabel={ratingLabel} size="lg" />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack direction="row" align="center" gap={4} wrap>
|
<Stack direction="row" align="center" gap={4} wrap>
|
||||||
<Text size="sm" color="text-gray-400">
|
<Text size="sm" color="text-gray-400">
|
||||||
{nationality}
|
{nationality}
|
||||||
</Text>
|
</Text>
|
||||||
{globalRank !== undefined && globalRank !== null && (
|
{globalRankLabel && (
|
||||||
<Text size="sm" color="text-gray-400">
|
<Text size="sm" color="text-gray-400">
|
||||||
Global Rank: <Text color="text-warning-amber" weight="semibold">#{globalRank}</Text>
|
Global Rank: <Text color="text-warning-amber" weight="semibold">{globalRankLabel}</Text>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ interface DriverProfileHeaderProps {
|
|||||||
avatarUrl?: string | null;
|
avatarUrl?: string | null;
|
||||||
nationality: string;
|
nationality: string;
|
||||||
rating: number;
|
rating: number;
|
||||||
|
ratingLabel: string;
|
||||||
safetyRating?: number;
|
safetyRating?: number;
|
||||||
globalRank?: number;
|
safetyRatingLabel: string;
|
||||||
|
globalRankLabel?: string;
|
||||||
bio?: string | null;
|
bio?: string | null;
|
||||||
friendRequestSent: boolean;
|
friendRequestSent: boolean;
|
||||||
onAddFriend: () => void;
|
onAddFriend: () => void;
|
||||||
@@ -26,8 +28,10 @@ export function DriverProfileHeader({
|
|||||||
avatarUrl,
|
avatarUrl,
|
||||||
nationality,
|
nationality,
|
||||||
rating,
|
rating,
|
||||||
|
ratingLabel,
|
||||||
safetyRating = 92,
|
safetyRating = 92,
|
||||||
globalRank,
|
safetyRatingLabel,
|
||||||
|
globalRankLabel,
|
||||||
bio,
|
bio,
|
||||||
friendRequestSent,
|
friendRequestSent,
|
||||||
onAddFriend,
|
onAddFriend,
|
||||||
@@ -56,11 +60,11 @@ export function DriverProfileHeader({
|
|||||||
<Stack>
|
<Stack>
|
||||||
<Stack direction="row" align="center" gap={3} mb={1}>
|
<Stack direction="row" align="center" gap={3} mb={1}>
|
||||||
<Heading level={1}>{name}</Heading>
|
<Heading level={1}>{name}</Heading>
|
||||||
{globalRank && (
|
{globalRankLabel && (
|
||||||
<Stack display="flex" alignItems="center" gap={1} rounded="md" bg="bg-warning-amber/10" px={2} py={0.5} border borderColor="border-warning-amber/20">
|
<Stack display="flex" alignItems="center" gap={1} rounded="md" bg="bg-warning-amber/10" px={2} py={0.5} border borderColor="border-warning-amber/20">
|
||||||
<Trophy size={12} color="#FFBE4D" />
|
<Trophy size={12} color="#FFBE4D" />
|
||||||
<Text size="xs" weight="bold" font="mono" color="text-warning-amber">
|
<Text size="xs" weight="bold" font="mono" color="text-warning-amber">
|
||||||
#{globalRank}
|
{globalRankLabel}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
@@ -72,8 +76,8 @@ export function DriverProfileHeader({
|
|||||||
</Stack>
|
</Stack>
|
||||||
<Stack w="1" h="1" rounded="full" bg="bg-gray-700" />
|
<Stack w="1" h="1" rounded="full" bg="bg-gray-700" />
|
||||||
<Stack direction="row" align="center" gap={2}>
|
<Stack direction="row" align="center" gap={2}>
|
||||||
<RatingBadge rating={rating} size="sm" />
|
<RatingBadge rating={rating} ratingLabel={ratingLabel} size="sm" />
|
||||||
<SafetyRatingBadge rating={safetyRating} size="sm" />
|
<SafetyRatingBadge rating={safetyRating} ratingLabel={safetyRatingLabel} size="sm" />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ interface DriverTableRowProps {
|
|||||||
avatarUrl?: string | null;
|
avatarUrl?: string | null;
|
||||||
nationality: string;
|
nationality: string;
|
||||||
rating: number;
|
rating: number;
|
||||||
wins: number;
|
ratingLabel: string;
|
||||||
|
winsLabel: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +24,8 @@ export function DriverTableRow({
|
|||||||
avatarUrl,
|
avatarUrl,
|
||||||
nationality,
|
nationality,
|
||||||
rating,
|
rating,
|
||||||
wins,
|
ratingLabel,
|
||||||
|
winsLabel,
|
||||||
onClick,
|
onClick,
|
||||||
}: DriverTableRowProps) {
|
}: DriverTableRowProps) {
|
||||||
return (
|
return (
|
||||||
@@ -58,11 +60,11 @@ export function DriverTableRow({
|
|||||||
<Text size="xs" variant="low">{nationality}</Text>
|
<Text size="xs" variant="low">{nationality}</Text>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell textAlign="right">
|
<TableCell textAlign="right">
|
||||||
<RatingBadge rating={rating} size="sm" />
|
<RatingBadge rating={rating} ratingLabel={ratingLabel} size="sm" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell textAlign="right">
|
<TableCell textAlign="right">
|
||||||
<Text size="sm" weight="semibold" font="mono" variant="success">
|
<Text size="sm" weight="semibold" font="mono" variant="success">
|
||||||
{wins}
|
{winsLabel}
|
||||||
</Text>
|
</Text>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -17,25 +17,25 @@ interface DriverStat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface DriversDirectoryHeaderProps {
|
interface DriversDirectoryHeaderProps {
|
||||||
totalDrivers: number;
|
totalDriversLabel: string;
|
||||||
activeDrivers: number;
|
activeDriversLabel: string;
|
||||||
totalWins: number;
|
totalWinsLabel: string;
|
||||||
totalRaces: number;
|
totalRacesLabel: string;
|
||||||
onViewLeaderboard: () => void;
|
onViewLeaderboard: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DriversDirectoryHeader({
|
export function DriversDirectoryHeader({
|
||||||
totalDrivers,
|
totalDriversLabel,
|
||||||
activeDrivers,
|
activeDriversLabel,
|
||||||
totalWins,
|
totalWinsLabel,
|
||||||
totalRaces,
|
totalRacesLabel,
|
||||||
onViewLeaderboard,
|
onViewLeaderboard,
|
||||||
}: DriversDirectoryHeaderProps) {
|
}: DriversDirectoryHeaderProps) {
|
||||||
const stats: DriverStat[] = [
|
const stats: DriverStat[] = [
|
||||||
{ label: 'drivers', value: totalDrivers, intent: 'primary' },
|
{ label: 'drivers', value: totalDriversLabel, intent: 'primary' },
|
||||||
{ label: 'active', value: activeDrivers, intent: 'success' },
|
{ label: 'active', value: activeDriversLabel, intent: 'success' },
|
||||||
{ label: 'total wins', value: totalWins.toLocaleString(), intent: 'warning' },
|
{ label: 'total wins', value: totalWinsLabel, intent: 'warning' },
|
||||||
{ label: 'races', value: totalRaces.toLocaleString(), intent: 'telemetry' },
|
{ label: 'races', value: totalRacesLabel, intent: 'telemetry' },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ interface FeaturedDriverCardProps {
|
|||||||
name: string;
|
name: string;
|
||||||
nationality: string;
|
nationality: string;
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
rating: number;
|
ratingLabel: string;
|
||||||
wins: number;
|
winsLabel: string;
|
||||||
podiums: number;
|
podiumsLabel: string;
|
||||||
skillLevel?: string;
|
skillLevel?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
};
|
};
|
||||||
@@ -142,17 +142,17 @@ export function FeaturedDriverCard({ driver, position, onClick }: FeaturedDriver
|
|||||||
<Box display="grid" gridCols={3} gap={3}>
|
<Box display="grid" gridCols={3} gap={3}>
|
||||||
<MiniStat
|
<MiniStat
|
||||||
label="Rating"
|
label="Rating"
|
||||||
value={driver.rating.toLocaleString()}
|
value={driver.ratingLabel}
|
||||||
color="text-primary-blue"
|
color="text-primary-blue"
|
||||||
/>
|
/>
|
||||||
<MiniStat
|
<MiniStat
|
||||||
label="Wins"
|
label="Wins"
|
||||||
value={driver.wins}
|
value={driver.winsLabel}
|
||||||
color="text-performance-green"
|
color="text-performance-green"
|
||||||
/>
|
/>
|
||||||
<MiniStat
|
<MiniStat
|
||||||
label="Podiums"
|
label="Podiums"
|
||||||
value={driver.podiums}
|
value={driver.podiumsLabel}
|
||||||
color="text-warning-amber"
|
color="text-warning-amber"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ interface ProfileHeroProps {
|
|||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
country: string;
|
country: string;
|
||||||
iracingId: number;
|
iracingId: number;
|
||||||
joinedAt: string | Date;
|
joinedAtLabel: string;
|
||||||
};
|
};
|
||||||
stats: {
|
stats: {
|
||||||
rating: number;
|
ratingLabel: string;
|
||||||
} | null;
|
} | null;
|
||||||
globalRank: number;
|
globalRankLabel: string;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
socialHandles: {
|
socialHandles: {
|
||||||
platform: string;
|
platform: string;
|
||||||
@@ -47,7 +47,7 @@ function getSocialIcon(platform: string) {
|
|||||||
export function ProfileHero({
|
export function ProfileHero({
|
||||||
driver,
|
driver,
|
||||||
stats,
|
stats,
|
||||||
globalRank,
|
globalRankLabel,
|
||||||
timezone,
|
timezone,
|
||||||
socialHandles,
|
socialHandles,
|
||||||
onAddFriend,
|
onAddFriend,
|
||||||
@@ -87,14 +87,14 @@ export function ProfileHero({
|
|||||||
<Surface variant="muted" rounded="lg" padding={1} style={{ backgroundColor: 'rgba(59, 130, 246, 0.1)', border: '1px solid rgba(59, 130, 246, 0.3)', paddingLeft: '0.75rem', paddingRight: '0.75rem' }}>
|
<Surface variant="muted" rounded="lg" padding={1} style={{ backgroundColor: 'rgba(59, 130, 246, 0.1)', border: '1px solid rgba(59, 130, 246, 0.3)', paddingLeft: '0.75rem', paddingRight: '0.75rem' }}>
|
||||||
<Stack direction="row" align="center" gap={2}>
|
<Stack direction="row" align="center" gap={2}>
|
||||||
<Star style={{ width: '1rem', height: '1rem', color: '#3b82f6' }} />
|
<Star style={{ width: '1rem', height: '1rem', color: '#3b82f6' }} />
|
||||||
<Text font="mono" weight="bold" color="text-primary-blue">{stats.rating}</Text>
|
<Text font="mono" weight="bold" color="text-primary-blue">{stats.ratingLabel}</Text>
|
||||||
<Text size="xs" color="text-gray-400">Rating</Text>
|
<Text size="xs" color="text-gray-400">Rating</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Surface>
|
</Surface>
|
||||||
<Surface variant="muted" rounded="lg" padding={1} style={{ backgroundColor: 'rgba(250, 204, 21, 0.1)', border: '1px solid rgba(250, 204, 21, 0.3)', paddingLeft: '0.75rem', paddingRight: '0.75rem' }}>
|
<Surface variant="muted" rounded="lg" padding={1} style={{ backgroundColor: 'rgba(250, 204, 21, 0.1)', border: '1px solid rgba(250, 204, 21, 0.3)', paddingLeft: '0.75rem', paddingRight: '0.75rem' }}>
|
||||||
<Stack direction="row" align="center" gap={2}>
|
<Stack direction="row" align="center" gap={2}>
|
||||||
<Trophy style={{ width: '1rem', height: '1rem', color: '#facc15' }} />
|
<Trophy style={{ width: '1rem', height: '1rem', color: '#facc15' }} />
|
||||||
<Text font="mono" weight="bold" style={{ color: '#facc15' }}>#{globalRank}</Text>
|
<Text font="mono" weight="bold" style={{ color: '#facc15' }}>{globalRankLabel}</Text>
|
||||||
<Text size="xs" color="text-gray-400">Global</Text>
|
<Text size="xs" color="text-gray-400">Global</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Surface>
|
</Surface>
|
||||||
@@ -111,11 +111,7 @@ export function ProfileHero({
|
|||||||
<Stack direction="row" align="center" gap={1.5}>
|
<Stack direction="row" align="center" gap={1.5}>
|
||||||
<Calendar style={{ width: '1rem', height: '1rem' }} />
|
<Calendar style={{ width: '1rem', height: '1rem' }} />
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
Joined{' '}
|
Joined {driver.joinedAtLabel}
|
||||||
{new Date(driver.joinedAt).toLocaleDateString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
year: 'numeric',
|
|
||||||
})}
|
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction="row" align="center" gap={1.5}>
|
<Stack direction="row" align="center" gap={1.5}>
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import { Badge } from '@/ui/Badge';
|
|||||||
|
|
||||||
interface RatingBadgeProps {
|
interface RatingBadgeProps {
|
||||||
rating: number;
|
rating: number;
|
||||||
|
ratingLabel: string;
|
||||||
size?: 'sm' | 'md' | 'lg';
|
size?: 'sm' | 'md' | 'lg';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RatingBadge({ rating, size = 'md' }: RatingBadgeProps) {
|
export function RatingBadge({ rating, ratingLabel, size = 'md' }: RatingBadgeProps) {
|
||||||
const badgeSize = size === 'lg' ? 'md' : size;
|
const badgeSize = size === 'lg' ? 'md' : size;
|
||||||
|
|
||||||
const getVariant = (val: number): 'warning' | 'primary' | 'success' | 'default' => {
|
const getVariant = (val: number): 'warning' | 'primary' | 'success' | 'default' => {
|
||||||
@@ -22,7 +23,7 @@ export function RatingBadge({ rating, size = 'md' }: RatingBadgeProps) {
|
|||||||
variant={getVariant(rating)}
|
variant={getVariant(rating)}
|
||||||
size={badgeSize}
|
size={badgeSize}
|
||||||
>
|
>
|
||||||
{rating.toLocaleString()}
|
{ratingLabel}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import { Shield } from 'lucide-react';
|
|||||||
|
|
||||||
interface SafetyRatingBadgeProps {
|
interface SafetyRatingBadgeProps {
|
||||||
rating: number;
|
rating: number;
|
||||||
|
ratingLabel: string;
|
||||||
size?: 'sm' | 'md' | 'lg';
|
size?: 'sm' | 'md' | 'lg';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SafetyRatingBadge({ rating, size = 'md' }: SafetyRatingBadgeProps) {
|
export function SafetyRatingBadge({ rating, ratingLabel, size = 'md' }: SafetyRatingBadgeProps) {
|
||||||
const getColor = (r: number) => {
|
const getColor = (r: number) => {
|
||||||
if (r >= 90) return 'text-performance-green';
|
if (r >= 90) return 'text-performance-green';
|
||||||
if (r >= 70) return 'text-warning-amber';
|
if (r >= 70) return 'text-warning-amber';
|
||||||
@@ -65,7 +66,7 @@ export function SafetyRatingBadge({ rating, size = 'md' }: SafetyRatingBadgeProp
|
|||||||
font="mono"
|
font="mono"
|
||||||
color={colorClass}
|
color={colorClass}
|
||||||
>
|
>
|
||||||
SR {rating.toFixed(0)}
|
{ratingLabel}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
||||||
|
import { DurationDisplay } from '@/lib/display-objects/DurationDisplay';
|
||||||
|
import { MemoryDisplay } from '@/lib/display-objects/MemoryDisplay';
|
||||||
|
import { PercentDisplay } from '@/lib/display-objects/PercentDisplay';
|
||||||
|
import { TimeDisplay } from '@/lib/display-objects/TimeDisplay';
|
||||||
|
|
||||||
interface ErrorAnalyticsDashboardProps {
|
interface ErrorAnalyticsDashboardProps {
|
||||||
/**
|
/**
|
||||||
* Auto-refresh interval in milliseconds
|
* Auto-refresh interval in milliseconds
|
||||||
@@ -41,16 +47,16 @@ interface ErrorAnalyticsDashboardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatDuration(duration: number): string {
|
function formatDuration(duration: number): string {
|
||||||
return duration.toFixed(2) + 'ms';
|
return DurationDisplay.formatMs(duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPercentage(value: number, total: number): string {
|
function formatPercentage(value: number, total: number): string {
|
||||||
if (total === 0) return '0%';
|
if (total === 0) return '0%';
|
||||||
return ((value / total) * 100).toFixed(1) + '%';
|
return PercentDisplay.format(value / total);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatMemory(bytes: number): string {
|
function formatMemory(bytes: number): string {
|
||||||
return (bytes / 1024 / 1024).toFixed(1) + 'MB';
|
return MemoryDisplay.formatMB(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PerformanceWithMemory extends Performance {
|
interface PerformanceWithMemory extends Performance {
|
||||||
@@ -321,7 +327,7 @@ export function ErrorAnalyticsDashboard({
|
|||||||
<Stack display="flex" justifyContent="between" alignItems="start" gap={2} mb={1}>
|
<Stack display="flex" justifyContent="between" alignItems="start" gap={2} mb={1}>
|
||||||
<Text size="xs" font="mono" weight="bold" color="text-red-400" truncate>{error.type}</Text>
|
<Text size="xs" font="mono" weight="bold" color="text-red-400" truncate>{error.type}</Text>
|
||||||
<Text size="xs" color="text-gray-500" fontSize="10px">
|
<Text size="xs" color="text-gray-500" fontSize="10px">
|
||||||
{new Date(error.timestamp).toLocaleTimeString()}
|
{DateDisplay.formatTime(error.timestamp)}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Text size="xs" color="text-gray-300" block mb={1}>{error.message}</Text>
|
<Text size="xs" color="text-gray-300" block mb={1}>{error.message}</Text>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Grid } from '@/ui/Grid';
|
|||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/Stack';
|
||||||
import { Section } from '@/ui/Section';
|
import { Section } from '@/ui/Section';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
|
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
||||||
|
|
||||||
interface FeedItemData {
|
interface FeedItemData {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -46,6 +47,16 @@ export function FeedLayout({
|
|||||||
upcomingRaces,
|
upcomingRaces,
|
||||||
latestResults
|
latestResults
|
||||||
}: FeedLayoutProps) {
|
}: FeedLayoutProps) {
|
||||||
|
const formattedUpcomingRaces = upcomingRaces.map(r => ({
|
||||||
|
...r,
|
||||||
|
formattedDate: DateDisplay.formatShort(r.scheduledAt),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const formattedLatestResults = latestResults.map(r => ({
|
||||||
|
...r,
|
||||||
|
formattedDate: DateDisplay.formatShort(r.scheduledAt),
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section className="mt-16 mb-20">
|
<Section className="mt-16 mb-20">
|
||||||
<Container>
|
<Container>
|
||||||
@@ -64,8 +75,8 @@ export function FeedLayout({
|
|||||||
</Card>
|
</Card>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack as="aside" gap={6}>
|
<Stack as="aside" gap={6}>
|
||||||
<UpcomingRacesSidebar races={upcomingRaces} />
|
<UpcomingRacesSidebar races={formattedUpcomingRaces} />
|
||||||
<LatestResultsSidebar results={latestResults} />
|
<LatestResultsSidebar results={formattedLatestResults} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Text } from '@/ui/Text';
|
|||||||
|
|
||||||
interface JoinRequestItemProps {
|
interface JoinRequestItemProps {
|
||||||
driverId: string;
|
driverId: string;
|
||||||
requestedAt: string | Date;
|
formattedRequestedAt: string;
|
||||||
onApprove: () => void;
|
onApprove: () => void;
|
||||||
onReject: () => void;
|
onReject: () => void;
|
||||||
isApproving?: boolean;
|
isApproving?: boolean;
|
||||||
@@ -13,7 +13,7 @@ interface JoinRequestItemProps {
|
|||||||
|
|
||||||
export function JoinRequestItem({
|
export function JoinRequestItem({
|
||||||
driverId,
|
driverId,
|
||||||
requestedAt,
|
formattedRequestedAt,
|
||||||
onApprove,
|
onApprove,
|
||||||
onReject,
|
onReject,
|
||||||
isApproving,
|
isApproving,
|
||||||
@@ -47,7 +47,7 @@ export function JoinRequestItem({
|
|||||||
<Stack flexGrow={1}>
|
<Stack flexGrow={1}>
|
||||||
<Text color="text-white" weight="medium" block>{driverId}</Text>
|
<Text color="text-white" weight="medium" block>{driverId}</Text>
|
||||||
<Text size="sm" color="text-gray-400" block>
|
<Text size="sm" color="text-gray-400" block>
|
||||||
Requested {new Date(requestedAt).toLocaleDateString()}
|
Requested {formattedRequestedAt}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ActivityFeedItem } from '@/components/feed/ActivityFeedItem';
|
import { ActivityFeedItem } from '@/components/feed/ActivityFeedItem';
|
||||||
import { useLeagueRaces } from "@/hooks/league/useLeagueRaces";
|
import { useLeagueRaces } from "@/hooks/league/useLeagueRaces";
|
||||||
import { LeagueActivityService } from '@/lib/services/league/LeagueActivityService';
|
import { LeagueActivityService } from '@/lib/services/league/LeagueActivityService';
|
||||||
|
import { RelativeTimeDisplay } from '@/lib/display-objects/RelativeTimeDisplay';
|
||||||
import { Icon } from '@/ui/Icon';
|
import { Icon } from '@/ui/Icon';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/Stack';
|
||||||
@@ -20,19 +21,6 @@ interface LeagueActivityFeedProps {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeAgo(timestamp: Date): string {
|
|
||||||
const diffMs = Date.now() - timestamp.getTime();
|
|
||||||
const diffMinutes = Math.floor(diffMs / 60000);
|
|
||||||
if (diffMinutes < 1) return 'Just now';
|
|
||||||
if (diffMinutes < 60) return `${diffMinutes} min ago`;
|
|
||||||
const diffHours = Math.floor(diffMinutes / 60);
|
|
||||||
if (diffHours < 24) return `${diffHours}h ago`;
|
|
||||||
const diffDays = Math.floor(diffHours / 24);
|
|
||||||
if (diffDays === 1) return 'Yesterday';
|
|
||||||
if (diffDays < 7) return `${diffDays}d ago`;
|
|
||||||
return timestamp.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LeagueActivityFeed({ leagueId, limit = 10 }: LeagueActivityFeedProps) {
|
export function LeagueActivityFeed({ leagueId, limit = 10 }: LeagueActivityFeedProps) {
|
||||||
const { data: raceList = [], isLoading } = useLeagueRaces(leagueId);
|
const { data: raceList = [], isLoading } = useLeagueRaces(leagueId);
|
||||||
|
|
||||||
@@ -140,7 +128,7 @@ function ActivityItem({ activity }: { activity: LeagueActivity }) {
|
|||||||
<ActivityFeedItem
|
<ActivityFeedItem
|
||||||
icon={getIcon()}
|
icon={getIcon()}
|
||||||
content={getContent()}
|
content={getContent()}
|
||||||
timestamp={timeAgo(activity.timestamp)}
|
timestamp={RelativeTimeDisplay.format(activity.timestamp, new Date())}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ import { Icon } from '@/ui/Icon';
|
|||||||
import { Card } from '@/ui/Card';
|
import { Card } from '@/ui/Card';
|
||||||
import { Grid } from '@/ui/Grid';
|
import { Grid } from '@/ui/Grid';
|
||||||
|
|
||||||
|
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
||||||
|
import { DurationDisplay } from '@/lib/display-objects/DurationDisplay';
|
||||||
|
|
||||||
interface LeagueReviewSummaryProps {
|
interface LeagueReviewSummaryProps {
|
||||||
form: LeagueConfigFormModel;
|
form: LeagueConfigFormModel;
|
||||||
presets: LeagueScoringPresetViewModel[];
|
presets: LeagueScoringPresetViewModel[];
|
||||||
@@ -139,11 +142,7 @@ export function LeagueReviewSummary({ form, presets }: LeagueReviewSummaryProps)
|
|||||||
|
|
||||||
const seasonStartLabel =
|
const seasonStartLabel =
|
||||||
timings.seasonStartDate
|
timings.seasonStartDate
|
||||||
? new Date(timings.seasonStartDate).toLocaleDateString(undefined, {
|
? DateDisplay.formatShort(timings.seasonStartDate)
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
})
|
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const stewardingLabel = (() => {
|
const stewardingLabel = (() => {
|
||||||
|
|||||||
@@ -194,17 +194,10 @@ export function LeagueSchedule({ leagueId, onRaceClick }: LeagueScheduleProps) {
|
|||||||
<Stack display="flex" alignItems="center" gap={3}>
|
<Stack display="flex" alignItems="center" gap={3}>
|
||||||
<Stack textAlign="right">
|
<Stack textAlign="right">
|
||||||
<Text color="text-white" weight="medium" block>
|
<Text color="text-white" weight="medium" block>
|
||||||
{race.scheduledAt.toLocaleDateString('en-US', {
|
{race.formattedDate}
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
})}
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" color="text-gray-400" block>
|
<Text size="sm" color="text-gray-400" block>
|
||||||
{race.scheduledAt.toLocaleTimeString([], {
|
{race.formattedTime}
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
})}
|
|
||||||
</Text>
|
</Text>
|
||||||
{isPast && race.status === 'completed' && (
|
{isPast && race.status === 'completed' && (
|
||||||
<Text size="xs" color="text-primary-blue" mt={1} block>View Results →</Text>
|
<Text size="xs" color="text-primary-blue" mt={1} block>View Results →</Text>
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ export function ReviewProtestModal({
|
|||||||
<Stack direction="row" align="center" justify="between">
|
<Stack direction="row" align="center" justify="between">
|
||||||
<Text size="sm" color="text-gray-400">Filed Date</Text>
|
<Text size="sm" color="text-gray-400">Filed Date</Text>
|
||||||
<Text size="sm" color="text-white" weight="medium">
|
<Text size="sm" color="text-white" weight="medium">
|
||||||
{new Date(protest.filedAt || protest.submittedAt).toLocaleString()}
|
{protest.formattedSubmittedAt}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction="row" align="center" justify="between">
|
<Stack direction="row" align="center" justify="between">
|
||||||
@@ -299,7 +299,7 @@ export function ReviewProtestModal({
|
|||||||
<Text size="sm" color="text-gray-400">Status</Text>
|
<Text size="sm" color="text-gray-400">Status</Text>
|
||||||
<Stack as="span" px={2} py={1} rounded="sm" bg="bg-orange-500/20">
|
<Stack as="span" px={2} py={1} rounded="sm" bg="bg-orange-500/20">
|
||||||
<Text size="xs" weight="medium" color="text-orange-400">
|
<Text size="xs" weight="medium" color="text-orange-400">
|
||||||
{protest.status}
|
{protest.statusDisplay}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ interface Race {
|
|||||||
name: string;
|
name: string;
|
||||||
track?: string;
|
track?: string;
|
||||||
car?: string;
|
car?: string;
|
||||||
scheduledAt: string;
|
formattedDate: string;
|
||||||
|
formattedTime: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
statusLabel: string;
|
||||||
sessionType?: string;
|
sessionType?: string;
|
||||||
isPast?: boolean;
|
isPast?: boolean;
|
||||||
}
|
}
|
||||||
@@ -33,19 +35,19 @@ export function ScheduleRaceCard({ race }: ScheduleRaceCardProps) {
|
|||||||
<Stack w="3" h="3" rounded="full" bg={race.isPast ? 'bg-performance-green' : 'bg-primary-blue'} />
|
<Stack w="3" h="3" rounded="full" bg={race.isPast ? 'bg-performance-green' : 'bg-primary-blue'} />
|
||||||
<Heading level={3} fontSize="lg">{race.name}</Heading>
|
<Heading level={3} fontSize="lg">{race.name}</Heading>
|
||||||
<Badge variant={race.status === 'completed' ? 'success' : 'primary'}>
|
<Badge variant={race.status === 'completed' ? 'success' : 'primary'}>
|
||||||
{race.status === 'completed' ? 'Completed' : 'Scheduled'}
|
{race.statusLabel}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Grid cols={4} gap={4}>
|
<Grid cols={4} gap={4}>
|
||||||
<Stack direction="row" align="center" gap={2}>
|
<Stack direction="row" align="center" gap={2}>
|
||||||
<Icon icon={Calendar} size={4} color="#9ca3af" />
|
<Icon icon={Calendar} size={4} color="#9ca3af" />
|
||||||
<Text size="sm" color="text-gray-300">{new Date(race.scheduledAt).toLocaleDateString()}</Text>
|
<Text size="sm" color="text-gray-300">{race.formattedDate}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack direction="row" align="center" gap={2}>
|
<Stack direction="row" align="center" gap={2}>
|
||||||
<Icon icon={Clock} size={4} color="#9ca3af" />
|
<Icon icon={Clock} size={4} color="#9ca3af" />
|
||||||
<Text size="sm" color="text-gray-300">{new Date(race.scheduledAt).toLocaleTimeString()}</Text>
|
<Text size="sm" color="text-gray-300">{race.formattedTime}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{race.track && (
|
{race.track && (
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ interface SponsorshipRequest {
|
|||||||
id: string;
|
id: string;
|
||||||
sponsorName: string;
|
sponsorName: string;
|
||||||
status: 'pending' | 'approved' | 'rejected';
|
status: 'pending' | 'approved' | 'rejected';
|
||||||
requestedAt: string;
|
statusLabel: string;
|
||||||
|
formattedRequestedAt: string;
|
||||||
slotName: string;
|
slotName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ export function SponsorshipRequestCard({ request }: SponsorshipRequestCardProps)
|
|||||||
<Icon icon={statusIcon} size={5} color={statusColor} />
|
<Icon icon={statusIcon} size={5} color={statusColor} />
|
||||||
<Text weight="semibold" color="text-white">{request.sponsorName}</Text>
|
<Text weight="semibold" color="text-white">{request.sponsorName}</Text>
|
||||||
<Badge variant={statusVariant}>
|
<Badge variant={statusVariant}>
|
||||||
{request.status}
|
{request.statusLabel}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ export function SponsorshipRequestCard({ request }: SponsorshipRequestCardProps)
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text size="xs" color="text-gray-400" block>
|
<Text size="xs" color="text-gray-400" block>
|
||||||
{new Date(request.requestedAt).toLocaleDateString()}
|
{request.formattedRequestedAt}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -55,12 +55,12 @@ interface StandingsTableProps {
|
|||||||
standings: Array<{
|
standings: Array<{
|
||||||
driverId: string;
|
driverId: string;
|
||||||
position: number;
|
position: number;
|
||||||
totalPoints: number;
|
positionLabel: string;
|
||||||
racesFinished: number;
|
totalPointsLabel: string;
|
||||||
racesStarted: number;
|
racesLabel: string;
|
||||||
avgFinish: number | null;
|
avgFinishLabel: string;
|
||||||
penaltyPoints: number;
|
penaltyPointsLabel: string;
|
||||||
bonusPoints: number;
|
bonusPointsLabel: string;
|
||||||
teamName?: string;
|
teamName?: string;
|
||||||
}>;
|
}>;
|
||||||
drivers: Array<{
|
drivers: Array<{
|
||||||
@@ -508,7 +508,7 @@ export function StandingsTable({
|
|||||||
'text-white'
|
'text-white'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{row.position}
|
{row.positionLabel}
|
||||||
</Stack>
|
</Stack>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
@@ -625,7 +625,7 @@ export function StandingsTable({
|
|||||||
{/* Total Points with Hover Action */}
|
{/* Total Points with Hover Action */}
|
||||||
<TableCell textAlign="right" position="relative">
|
<TableCell textAlign="right" position="relative">
|
||||||
<Stack display="flex" alignItems="center" justifyContent="end" gap={2}>
|
<Stack display="flex" alignItems="center" justifyContent="end" gap={2}>
|
||||||
<Text color="text-white" weight="bold" size="lg">{row.totalPoints}</Text>
|
<Text color="text-white" weight="bold" size="lg">{row.totalPointsLabel}</Text>
|
||||||
{isAdmin && canModify && (
|
{isAdmin && canModify && (
|
||||||
<Stack
|
<Stack
|
||||||
as="button"
|
as="button"
|
||||||
@@ -650,28 +650,27 @@ export function StandingsTable({
|
|||||||
|
|
||||||
{/* Races (Finished/Started) */}
|
{/* Races (Finished/Started) */}
|
||||||
<TableCell textAlign="center">
|
<TableCell textAlign="center">
|
||||||
<Text color="text-white">{row.racesFinished}</Text>
|
<Text color="text-white">{row.racesLabel}</Text>
|
||||||
<Text color="text-gray-500">/{row.racesStarted}</Text>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
{/* Avg Finish */}
|
{/* Avg Finish */}
|
||||||
<TableCell textAlign="right">
|
<TableCell textAlign="right">
|
||||||
<Text color="text-gray-300">
|
<Text color="text-gray-300">
|
||||||
{row.avgFinish !== null ? row.avgFinish.toFixed(1) : '—'}
|
{row.avgFinishLabel}
|
||||||
</Text>
|
</Text>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
{/* Penalty */}
|
{/* Penalty */}
|
||||||
<TableCell textAlign="right">
|
<TableCell textAlign="right">
|
||||||
<Text color={row.penaltyPoints > 0 ? 'text-red-400' : 'text-gray-500'} weight={row.penaltyPoints > 0 ? 'medium' : 'normal'}>
|
<Text color="text-gray-500">
|
||||||
{row.penaltyPoints > 0 ? `-${row.penaltyPoints}` : '—'}
|
{row.penaltyPointsLabel}
|
||||||
</Text>
|
</Text>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
{/* Bonus */}
|
{/* Bonus */}
|
||||||
<TableCell textAlign="right">
|
<TableCell textAlign="right">
|
||||||
<Text color={row.bonusPoints !== 0 ? 'text-green-400' : 'text-gray-500'} weight={row.bonusPoints !== 0 ? 'medium' : 'normal'}>
|
<Text color="text-gray-500">
|
||||||
{row.bonusPoints !== 0 ? `+${row.bonusPoints}` : '—'}
|
{row.bonusPointsLabel}
|
||||||
</Text>
|
</Text>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface Protest {
|
|||||||
protestingDriver: string;
|
protestingDriver: string;
|
||||||
accusedDriver: string;
|
accusedDriver: string;
|
||||||
description: string;
|
description: string;
|
||||||
submittedAt: string;
|
formattedSubmittedAt: string;
|
||||||
status: 'pending' | 'under_review' | 'resolved' | 'rejected';
|
status: 'pending' | 'under_review' | 'resolved' | 'rejected';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ export function StewardingQueuePanel({ protests, onReview }: StewardingQueuePane
|
|||||||
<Stack direction="row" align="center" gap={1.5}>
|
<Stack direction="row" align="center" gap={1.5}>
|
||||||
<Icon icon={Clock} size={3} color="text-gray-600" />
|
<Icon icon={Clock} size={3} color="text-gray-600" />
|
||||||
<Text size="xs" color="text-gray-500">
|
<Text size="xs" color="text-gray-500">
|
||||||
{new Date(protest.submittedAt).toLocaleString()}
|
{protest.formattedSubmittedAt}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -9,21 +9,21 @@ import { ArrowDownLeft, ArrowUpRight, History, Wallet } from 'lucide-react';
|
|||||||
|
|
||||||
interface Transaction {
|
interface Transaction {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'credit' | 'debit';
|
type: 'deposit' | 'withdrawal' | 'sponsorship' | 'prize';
|
||||||
amount: number;
|
formattedAmount: string;
|
||||||
description: string;
|
description: string;
|
||||||
date: string;
|
formattedDate: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WalletSummaryPanelProps {
|
interface WalletSummaryPanelProps {
|
||||||
balance: number;
|
formattedBalance: string;
|
||||||
currency: string;
|
currency: string;
|
||||||
transactions: Transaction[];
|
transactions: Transaction[];
|
||||||
onDeposit: () => void;
|
onDeposit: () => void;
|
||||||
onWithdraw: () => void;
|
onWithdraw: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WalletSummaryPanel({ balance, currency, transactions, onDeposit, onWithdraw }: WalletSummaryPanelProps) {
|
export function WalletSummaryPanel({ formattedBalance, currency, transactions, onDeposit, onWithdraw }: WalletSummaryPanelProps) {
|
||||||
return (
|
return (
|
||||||
<Stack gap={6}>
|
<Stack gap={6}>
|
||||||
<Surface variant="dark" border rounded="lg" padding={8} position="relative" overflow="hidden">
|
<Surface variant="dark" border rounded="lg" padding={8} position="relative" overflow="hidden">
|
||||||
@@ -48,7 +48,7 @@ export function WalletSummaryPanel({ balance, currency, transactions, onDeposit,
|
|||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction="row" align="baseline" gap={2}>
|
<Stack direction="row" align="baseline" gap={2}>
|
||||||
<Text size="4xl" weight="bold" color="text-white">
|
<Text size="4xl" weight="bold" color="text-white">
|
||||||
{balance.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
{formattedBalance}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xl" weight="medium" color="text-gray-500">{currency}</Text>
|
<Text size="xl" weight="medium" color="text-gray-500">{currency}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -87,37 +87,40 @@ export function WalletSummaryPanel({ balance, currency, transactions, onDeposit,
|
|||||||
<Text color="text-gray-500">No recent transactions.</Text>
|
<Text color="text-gray-500">No recent transactions.</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
transactions.map((tx) => (
|
transactions.map((tx) => {
|
||||||
<Stack key={tx.id} p={4} borderBottom borderColor="border-charcoal-outline" hoverBg="bg-white/5" transition>
|
const isCredit = tx.type === 'deposit' || tx.type === 'sponsorship';
|
||||||
<Stack direction="row" justify="between" align="center">
|
return (
|
||||||
<Stack direction="row" align="center" gap={4}>
|
<Stack key={tx.id} p={4} borderBottom borderColor="border-charcoal-outline" hoverBg="bg-white/5" transition>
|
||||||
<Stack
|
<Stack direction="row" justify="between" align="center">
|
||||||
center
|
<Stack direction="row" align="center" gap={4}>
|
||||||
w={10}
|
<Stack
|
||||||
h={10}
|
center
|
||||||
rounded="full"
|
w={10}
|
||||||
bg={tx.type === 'credit' ? 'bg-performance-green/10' : 'bg-error-red/10'}
|
h={10}
|
||||||
|
rounded="full"
|
||||||
|
bg={isCredit ? 'bg-performance-green/10' : 'bg-error-red/10'}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={isCredit ? ArrowDownLeft : ArrowUpRight}
|
||||||
|
size={4}
|
||||||
|
color={isCredit ? 'text-performance-green' : 'text-error-red'}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack gap={0.5}>
|
||||||
|
<Text weight="medium" color="text-white">{tx.description}</Text>
|
||||||
|
<Text size="xs" color="text-gray-500">{tx.formattedDate}</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<Text
|
||||||
|
weight="bold"
|
||||||
|
color={isCredit ? 'text-performance-green' : 'text-white'}
|
||||||
>
|
>
|
||||||
<Icon
|
{tx.formattedAmount}
|
||||||
icon={tx.type === 'credit' ? ArrowDownLeft : ArrowUpRight}
|
</Text>
|
||||||
size={4}
|
|
||||||
color={tx.type === 'credit' ? 'text-performance-green' : 'text-error-red'}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<Stack gap={0.5}>
|
|
||||||
<Text weight="medium" color="text-white">{tx.description}</Text>
|
|
||||||
<Text size="xs" color="text-gray-500">{new Date(tx.date).toLocaleDateString()}</Text>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
<Text
|
|
||||||
weight="bold"
|
|
||||||
color={tx.type === 'credit' ? 'text-performance-green' : 'text-white'}
|
|
||||||
>
|
|
||||||
{tx.type === 'credit' ? '+' : '-'}{tx.amount.toFixed(2)}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
);
|
||||||
))
|
})
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Surface>
|
</Surface>
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ interface ProfileHeaderProps {
|
|||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
country: string;
|
country: string;
|
||||||
iracingId: number;
|
iracingId: number;
|
||||||
joinedAt: string | Date;
|
joinedAtLabel: string;
|
||||||
};
|
};
|
||||||
stats: {
|
stats: {
|
||||||
rating: number;
|
ratingLabel: string;
|
||||||
} | null;
|
} | null;
|
||||||
globalRank: number;
|
globalRankLabel: string;
|
||||||
onAddFriend?: () => void;
|
onAddFriend?: () => void;
|
||||||
friendRequestSent?: boolean;
|
friendRequestSent?: boolean;
|
||||||
isOwnProfile?: boolean;
|
isOwnProfile?: boolean;
|
||||||
@@ -33,7 +33,7 @@ interface ProfileHeaderProps {
|
|||||||
export function ProfileHeader({
|
export function ProfileHeader({
|
||||||
driver,
|
driver,
|
||||||
stats,
|
stats,
|
||||||
globalRank,
|
globalRankLabel,
|
||||||
onAddFriend,
|
onAddFriend,
|
||||||
friendRequestSent,
|
friendRequestSent,
|
||||||
isOwnProfile,
|
isOwnProfile,
|
||||||
@@ -69,7 +69,7 @@ export function ProfileHeader({
|
|||||||
<Group gap={1.5}>
|
<Group gap={1.5}>
|
||||||
<Calendar size={14} color="var(--ui-color-text-low)" />
|
<Calendar size={14} color="var(--ui-color-text-low)" />
|
||||||
<Text size="xs" variant="low">
|
<Text size="xs" variant="low">
|
||||||
Joined {new Date(driver.joinedAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
|
Joined {driver.joinedAtLabel}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -78,8 +78,8 @@ export function ProfileHeader({
|
|||||||
<ProfileStatsGroup>
|
<ProfileStatsGroup>
|
||||||
{stats && (
|
{stats && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<ProfileStat label="RATING" value={stats.rating} intent="primary" />
|
<ProfileStat label="RATING" value={stats.ratingLabel} intent="primary" />
|
||||||
<ProfileStat label="GLOBAL RANK" value={`#${globalRank}`} intent="warning" />
|
<ProfileStat label="GLOBAL RANK" value={globalRankLabel} intent="warning" />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
</ProfileStatsGroup>
|
</ProfileStatsGroup>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ type RaceWithResults = {
|
|||||||
track: string;
|
track: string;
|
||||||
car: string;
|
car: string;
|
||||||
winnerName: string;
|
winnerName: string;
|
||||||
scheduledAt: string | Date;
|
formattedDate: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface LatestResultsSidebarProps {
|
interface LatestResultsSidebarProps {
|
||||||
@@ -28,14 +28,12 @@ export function LatestResultsSidebar({ results }: LatestResultsSidebarProps) {
|
|||||||
</Heading>
|
</Heading>
|
||||||
<RaceResultList>
|
<RaceResultList>
|
||||||
{results.slice(0, 4).map((result) => {
|
{results.slice(0, 4).map((result) => {
|
||||||
const scheduledAt = typeof result.scheduledAt === 'string' ? new Date(result.scheduledAt) : result.scheduledAt;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box as="li" key={result.raceId}>
|
<Box as="li" key={result.raceId}>
|
||||||
<RaceSummaryItem
|
<RaceSummaryItem
|
||||||
track={result.track}
|
track={result.track}
|
||||||
meta={`${result.winnerName} • ${result.car}`}
|
meta={`${result.winnerName} • ${result.car}`}
|
||||||
dateLabel={scheduledAt.toLocaleDateString()}
|
dateLabel={result.formattedDate}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import { Calendar, Car, Clock, LucideIcon } from 'lucide-react';
|
|||||||
|
|
||||||
interface RaceHeroProps {
|
interface RaceHeroProps {
|
||||||
track: string;
|
track: string;
|
||||||
scheduledAt: string;
|
formattedDate: string;
|
||||||
|
formattedTime: string;
|
||||||
car: string;
|
car: string;
|
||||||
status: 'scheduled' | 'running' | 'completed' | 'cancelled';
|
status: 'scheduled' | 'running' | 'completed' | 'cancelled';
|
||||||
statusConfig: {
|
statusConfig: {
|
||||||
@@ -20,9 +21,8 @@ interface RaceHeroProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RaceHero({ track, scheduledAt, car, status, statusConfig }: RaceHeroProps) {
|
export function RaceHero({ track, formattedDate, formattedTime, car, status, statusConfig }: RaceHeroProps) {
|
||||||
const StatusIcon = statusConfig.icon;
|
const StatusIcon = statusConfig.icon;
|
||||||
const date = new Date(scheduledAt);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Hero variant="primary">
|
<Hero variant="primary">
|
||||||
@@ -59,11 +59,11 @@ export function RaceHero({ track, scheduledAt, car, status, statusConfig }: Race
|
|||||||
<Stack direction="row" align="center" gap={6} wrap>
|
<Stack direction="row" align="center" gap={6} wrap>
|
||||||
<Stack direction="row" align="center" gap={2}>
|
<Stack direction="row" align="center" gap={2}>
|
||||||
<Icon icon={Calendar} size={4} color="rgb(156, 163, 175)" />
|
<Icon icon={Calendar} size={4} color="rgb(156, 163, 175)" />
|
||||||
<Text color="text-gray-400">{date.toLocaleDateString()}</Text>
|
<Text color="text-gray-400">{formattedDate}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction="row" align="center" gap={2}>
|
<Stack direction="row" align="center" gap={2}>
|
||||||
<Icon icon={Clock} size={4} color="rgb(156, 163, 175)" />
|
<Icon icon={Clock} size={4} color="rgb(156, 163, 175)" />
|
||||||
<Text color="text-gray-400">{date.toLocaleTimeString()}</Text>
|
<Text color="text-gray-400">{formattedTime}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction="row" align="center" gap={2}>
|
<Stack direction="row" align="center" gap={2}>
|
||||||
<Icon icon={Car} size={4} color="rgb(156, 163, 175)" />
|
<Icon icon={Car} size={4} color="rgb(156, 163, 175)" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { RaceHero as UiRaceHero } from '@/components/races/RaceHero';
|
import { RaceHero as UiRaceHero } from '@/components/races/RaceHero';
|
||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
||||||
|
|
||||||
interface RaceHeroProps {
|
interface RaceHeroProps {
|
||||||
track: string;
|
track: string;
|
||||||
@@ -17,7 +18,7 @@ interface RaceHeroProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function RaceHero(props: RaceHeroProps) {
|
export function RaceHero(props: RaceHeroProps) {
|
||||||
const { statusConfig, ...rest } = props;
|
const { statusConfig, scheduledAt, ...rest } = props;
|
||||||
|
|
||||||
// Map variant to match UI component expectations
|
// Map variant to match UI component expectations
|
||||||
const mappedConfig: {
|
const mappedConfig: {
|
||||||
@@ -30,5 +31,12 @@ export function RaceHero(props: RaceHeroProps) {
|
|||||||
variant: statusConfig.variant === 'default' ? 'default' : statusConfig.variant
|
variant: statusConfig.variant === 'default' ? 'default' : statusConfig.variant
|
||||||
};
|
};
|
||||||
|
|
||||||
return <UiRaceHero {...rest} statusConfig={mappedConfig} />;
|
return (
|
||||||
|
<UiRaceHero
|
||||||
|
{...rest}
|
||||||
|
formattedDate={DateDisplay.formatShort(scheduledAt)}
|
||||||
|
formattedTime={DateDisplay.formatTime(scheduledAt)}
|
||||||
|
statusConfig={mappedConfig}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
import { routes } from '@/lib/routing/RouteConfig';
|
import { routes } from '@/lib/routing/RouteConfig';
|
||||||
import { RaceListItem as UiRaceListItem } from '@/components/races/RaceListItem';
|
import { RaceListItem as UiRaceListItem } from '@/components/races/RaceListItem';
|
||||||
import { CheckCircle2, Clock, PlayCircle, XCircle } from 'lucide-react';
|
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
||||||
|
import { StatusDisplay } from '@/lib/display-objects/StatusDisplay';
|
||||||
|
|
||||||
interface Race {
|
interface Race {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -26,45 +27,32 @@ export function RaceListItem({ race, onClick }: RaceListItemProps) {
|
|||||||
scheduled: {
|
scheduled: {
|
||||||
iconName: 'Clock',
|
iconName: 'Clock',
|
||||||
variant: 'primary' as const,
|
variant: 'primary' as const,
|
||||||
label: 'Scheduled',
|
|
||||||
},
|
},
|
||||||
running: {
|
running: {
|
||||||
iconName: 'PlayCircle',
|
iconName: 'PlayCircle',
|
||||||
variant: 'success' as const,
|
variant: 'success' as const,
|
||||||
label: 'LIVE',
|
|
||||||
},
|
},
|
||||||
completed: {
|
completed: {
|
||||||
iconName: 'CheckCircle2',
|
iconName: 'CheckCircle2',
|
||||||
variant: 'default' as const,
|
variant: 'default' as const,
|
||||||
label: 'Completed',
|
|
||||||
},
|
},
|
||||||
cancelled: {
|
cancelled: {
|
||||||
iconName: 'XCircle',
|
iconName: 'XCircle',
|
||||||
variant: 'warning' as const,
|
variant: 'warning' as const,
|
||||||
label: 'Cancelled',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = statusConfig[race.status];
|
const config = statusConfig[race.status];
|
||||||
|
|
||||||
const formatTime = (date: string) => {
|
|
||||||
return new Date(date).toLocaleTimeString('en-US', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const date = new Date(race.scheduledAt);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UiRaceListItem
|
<UiRaceListItem
|
||||||
track={race.track}
|
track={race.track}
|
||||||
car={race.car}
|
car={race.car}
|
||||||
dateLabel={date.toLocaleDateString('en-US', { month: 'short' })}
|
dateLabel={DateDisplay.formatMonthDay(race.scheduledAt).split(' ')[0]}
|
||||||
dayLabel={date.getDate().toString()}
|
dayLabel={DateDisplay.formatMonthDay(race.scheduledAt).split(' ')[1]}
|
||||||
timeLabel={formatTime(race.scheduledAt)}
|
timeLabel={DateDisplay.formatTime(race.scheduledAt)}
|
||||||
status={race.status}
|
status={race.status}
|
||||||
statusLabel={config.label}
|
statusLabel={StatusDisplay.raceStatus(race.status)}
|
||||||
statusVariant={config.variant}
|
statusVariant={config.variant}
|
||||||
statusIconName={config.iconName}
|
statusIconName={config.iconName}
|
||||||
leagueName={race.leagueName}
|
leagueName={race.leagueName}
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ interface RaceResultCardProps {
|
|||||||
raceId: string;
|
raceId: string;
|
||||||
track: string;
|
track: string;
|
||||||
car: string;
|
car: string;
|
||||||
scheduledAt: string | Date;
|
formattedDate: string;
|
||||||
position: number;
|
position: number;
|
||||||
startPosition: number;
|
positionLabel: string;
|
||||||
incidents: number;
|
startPositionLabel: string;
|
||||||
|
incidentsLabel: string;
|
||||||
|
positionsGainedLabel?: string;
|
||||||
leagueName?: string;
|
leagueName?: string;
|
||||||
showLeague?: boolean;
|
showLeague?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
@@ -25,10 +27,12 @@ export function RaceResultCard({
|
|||||||
raceId,
|
raceId,
|
||||||
track,
|
track,
|
||||||
car,
|
car,
|
||||||
scheduledAt,
|
formattedDate,
|
||||||
position,
|
position,
|
||||||
startPosition,
|
positionLabel,
|
||||||
incidents,
|
startPositionLabel,
|
||||||
|
incidentsLabel,
|
||||||
|
positionsGainedLabel,
|
||||||
leagueName,
|
leagueName,
|
||||||
showLeague = true,
|
showLeague = true,
|
||||||
onClick,
|
onClick,
|
||||||
@@ -66,7 +70,7 @@ export function RaceResultCard({
|
|||||||
border
|
border
|
||||||
borderColor="border-outline-steel"
|
borderColor="border-outline-steel"
|
||||||
>
|
>
|
||||||
P{position}
|
{positionLabel}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text color="text-white" weight="medium" block groupHoverTextColor="text-primary-accent" transition>
|
<Text color="text-white" weight="medium" block groupHoverTextColor="text-primary-accent" transition>
|
||||||
@@ -78,11 +82,7 @@ export function RaceResultCard({
|
|||||||
<Stack direction="row" align="center" gap={3}>
|
<Stack direction="row" align="center" gap={3}>
|
||||||
<Stack textAlign="right">
|
<Stack textAlign="right">
|
||||||
<Text size="sm" color="text-gray-400" block>
|
<Text size="sm" color="text-gray-400" block>
|
||||||
{new Date(scheduledAt).toLocaleDateString('en-US', {
|
{formattedDate}
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
})}
|
|
||||||
</Text>
|
</Text>
|
||||||
{showLeague && leagueName && (
|
{showLeague && leagueName && (
|
||||||
<Text size="xs" color="text-gray-500" block>{leagueName}</Text>
|
<Text size="xs" color="text-gray-500" block>{leagueName}</Text>
|
||||||
@@ -92,16 +92,16 @@ export function RaceResultCard({
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction="row" align="center" gap={4}>
|
<Stack direction="row" align="center" gap={4}>
|
||||||
<Text size="xs" color="text-gray-500">Started P{startPosition}</Text>
|
<Text size="xs" color="text-gray-500">Started {startPositionLabel}</Text>
|
||||||
<Text size="xs" color="text-gray-500">•</Text>
|
<Text size="xs" color="text-gray-500">•</Text>
|
||||||
<Text size="xs" color={incidents === 0 ? 'text-success-green' : incidents > 2 ? 'text-error-red' : 'text-gray-500'}>
|
<Text size="xs" color="text-gray-500">
|
||||||
{incidents}x incidents
|
{incidentsLabel}
|
||||||
</Text>
|
</Text>
|
||||||
{position < startPosition && (
|
{positionsGainedLabel && (
|
||||||
<>
|
<>
|
||||||
<Text size="xs" color="text-gray-500">•</Text>
|
<Text size="xs" color="text-gray-500">•</Text>
|
||||||
<Text size="xs" color="text-success-green">
|
<Text size="xs" color="text-success-green">
|
||||||
+{startPosition - position} positions
|
{positionsGainedLabel}
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { RaceResultViewModel } from '@/lib/view-models/RaceResultViewModel';
|
import { RaceResultViewModel } from '@/lib/view-models/RaceResultViewModel';
|
||||||
import { RaceResultCard as UiRaceResultCard } from './RaceResultCard';
|
import { RaceResultCard as UiRaceResultCard } from './RaceResultCard';
|
||||||
|
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
||||||
|
|
||||||
interface RaceResultCardProps {
|
interface RaceResultCardProps {
|
||||||
race: {
|
race: {
|
||||||
@@ -28,10 +29,12 @@ export function RaceResultCard({
|
|||||||
raceId={race.id}
|
raceId={race.id}
|
||||||
track={race.track}
|
track={race.track}
|
||||||
car={race.car}
|
car={race.car}
|
||||||
scheduledAt={race.scheduledAt}
|
formattedDate={DateDisplay.formatShort(race.scheduledAt)}
|
||||||
position={result.position}
|
position={result.position}
|
||||||
startPosition={result.startPosition}
|
positionLabel={result.formattedPosition}
|
||||||
incidents={result.incidents}
|
startPositionLabel={result.formattedStartPosition}
|
||||||
|
incidentsLabel={result.formattedIncidents}
|
||||||
|
positionsGainedLabel={result.formattedPositionsGained}
|
||||||
leagueName={league?.name}
|
leagueName={league?.name}
|
||||||
showLeague={showLeague}
|
showLeague={showLeague}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ type UpcomingRace = {
|
|||||||
id: string;
|
id: string;
|
||||||
track: string;
|
track: string;
|
||||||
car: string;
|
car: string;
|
||||||
scheduledAt: string | Date;
|
formattedDate: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface UpcomingRacesSidebarProps {
|
interface UpcomingRacesSidebarProps {
|
||||||
@@ -35,14 +35,12 @@ export function UpcomingRacesSidebar({ races }: UpcomingRacesSidebarProps) {
|
|||||||
</Stack>
|
</Stack>
|
||||||
<Stack gap={3}>
|
<Stack gap={3}>
|
||||||
{races.slice(0, 4).map((race) => {
|
{races.slice(0, 4).map((race) => {
|
||||||
const scheduledAt = typeof race.scheduledAt === 'string' ? new Date(race.scheduledAt) : race.scheduledAt;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RaceSummaryItem
|
<RaceSummaryItem
|
||||||
key={race.id}
|
key={race.id}
|
||||||
track={race.track}
|
track={race.track}
|
||||||
meta={race.car}
|
meta={race.car}
|
||||||
dateLabel={scheduledAt.toLocaleDateString()}
|
dateLabel={race.formattedDate}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface PricingTier {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
price: number;
|
price: number;
|
||||||
|
priceLabel: string;
|
||||||
period: string;
|
period: string;
|
||||||
description: string;
|
description: string;
|
||||||
features: string[];
|
features: string[];
|
||||||
@@ -69,7 +70,7 @@ export function PricingTableShell({ title, tiers, onSelect, selectedId }: Pricin
|
|||||||
{tier.name}
|
{tier.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Stack direction="row" align="baseline" gap={1}>
|
<Stack direction="row" align="baseline" gap={1}>
|
||||||
<Text size="3xl" weight="bold" color="text-white">${tier.price}</Text>
|
<Text size="3xl" weight="bold" color="text-white">{tier.priceLabel}</Text>
|
||||||
<Text size="sm" color="text-gray-500">/{tier.period}</Text>
|
<Text size="sm" color="text-gray-500">/{tier.period}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Text size="sm" color="text-gray-400" block mt={2}>
|
<Text size="sm" color="text-gray-400" block mt={2}>
|
||||||
|
|||||||
@@ -1,62 +1,61 @@
|
|||||||
export interface SponsorshipSlot {
|
export interface SponsorshipSlot {
|
||||||
tier: 'main' | 'secondary';
|
tier: 'main' | 'secondary';
|
||||||
available: boolean;
|
available: boolean;
|
||||||
price: number;
|
priceLabel: string;
|
||||||
currency?: string;
|
|
||||||
benefits: string[];
|
benefits: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SlotTemplates = {
|
export const SlotTemplates = {
|
||||||
league: (mainAvailable: boolean, secondaryAvailable: number, mainPrice: number, secondaryPrice: number): SponsorshipSlot[] => [
|
league: (mainAvailable: boolean, secondaryAvailable: number, mainPriceLabel: string, secondaryPriceLabel: string): SponsorshipSlot[] => [
|
||||||
{
|
{
|
||||||
tier: 'main',
|
tier: 'main',
|
||||||
available: mainAvailable,
|
available: mainAvailable,
|
||||||
price: mainPrice,
|
priceLabel: mainPriceLabel,
|
||||||
benefits: ['Hood placement', 'League banner', 'Prominent logo'],
|
benefits: ['Hood placement', 'League banner', 'Prominent logo'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tier: 'secondary',
|
tier: 'secondary',
|
||||||
available: secondaryAvailable > 0,
|
available: secondaryAvailable > 0,
|
||||||
price: secondaryPrice,
|
priceLabel: secondaryPriceLabel,
|
||||||
benefits: ['Side logo placement', 'League page listing'],
|
benefits: ['Side logo placement', 'League page listing'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tier: 'secondary',
|
tier: 'secondary',
|
||||||
available: secondaryAvailable > 1,
|
available: secondaryAvailable > 1,
|
||||||
price: secondaryPrice,
|
priceLabel: secondaryPriceLabel,
|
||||||
benefits: ['Side logo placement', 'League page listing'],
|
benefits: ['Side logo placement', 'League page listing'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
race: (mainAvailable: boolean, mainPrice: number): SponsorshipSlot[] => [
|
race: (mainAvailable: boolean, mainPriceLabel: string): SponsorshipSlot[] => [
|
||||||
{
|
{
|
||||||
tier: 'main',
|
tier: 'main',
|
||||||
available: mainAvailable,
|
available: mainAvailable,
|
||||||
price: mainPrice,
|
priceLabel: mainPriceLabel,
|
||||||
benefits: ['Race title sponsor', 'Stream overlay', 'Results banner'],
|
benefits: ['Race title sponsor', 'Stream overlay', 'Results banner'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
driver: (available: boolean, price: number): SponsorshipSlot[] => [
|
driver: (available: boolean, priceLabel: string): SponsorshipSlot[] => [
|
||||||
{
|
{
|
||||||
tier: 'main',
|
tier: 'main',
|
||||||
available,
|
available,
|
||||||
price,
|
priceLabel,
|
||||||
benefits: ['Suit logo', 'Helmet branding', 'Social mentions'],
|
benefits: ['Suit logo', 'Helmet branding', 'Social mentions'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
team: (mainAvailable: boolean, secondaryAvailable: boolean, mainPrice: number, secondaryPrice: number): SponsorshipSlot[] => [
|
team: (mainAvailable: boolean, secondaryAvailable: boolean, mainPriceLabel: string, secondaryPriceLabel: string): SponsorshipSlot[] => [
|
||||||
{
|
{
|
||||||
tier: 'main',
|
tier: 'main',
|
||||||
available: mainAvailable,
|
available: mainAvailable,
|
||||||
price: mainPrice,
|
priceLabel: mainPriceLabel,
|
||||||
benefits: ['Team name suffix', 'Car livery', 'All driver suits'],
|
benefits: ['Team name suffix', 'Car livery', 'All driver suits'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tier: 'secondary',
|
tier: 'secondary',
|
||||||
available: secondaryAvailable,
|
available: secondaryAvailable,
|
||||||
price: secondaryPrice,
|
priceLabel: secondaryPriceLabel,
|
||||||
benefits: ['Team page logo', 'Minor livery placement'],
|
benefits: ['Team page logo', 'Minor livery placement'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -47,11 +47,11 @@ export interface SponsorInsightsProps {
|
|||||||
slots: SponsorshipSlot[];
|
slots: SponsorshipSlot[];
|
||||||
additionalStats?: {
|
additionalStats?: {
|
||||||
label: string;
|
label: string;
|
||||||
items: Array<{ label: string; value: string | number }>;
|
items: Array<{ label: string; value: string }>;
|
||||||
};
|
};
|
||||||
trustScore?: number;
|
trustScoreLabel?: string;
|
||||||
discordMembers?: number;
|
discordMembersLabel?: string;
|
||||||
monthlyActivity?: number;
|
monthlyActivityLabel?: string;
|
||||||
ctaLabel?: string;
|
ctaLabel?: string;
|
||||||
ctaHref?: string;
|
ctaHref?: string;
|
||||||
currentSponsorId?: string;
|
currentSponsorId?: string;
|
||||||
@@ -67,9 +67,9 @@ export function SponsorInsightsCard({
|
|||||||
metrics,
|
metrics,
|
||||||
slots,
|
slots,
|
||||||
additionalStats,
|
additionalStats,
|
||||||
trustScore,
|
trustScoreLabel,
|
||||||
discordMembers,
|
discordMembersLabel,
|
||||||
monthlyActivity,
|
monthlyActivityLabel,
|
||||||
ctaLabel,
|
ctaLabel,
|
||||||
ctaHref,
|
ctaHref,
|
||||||
currentSponsorId,
|
currentSponsorId,
|
||||||
@@ -111,22 +111,10 @@ export function SponsorInsightsCard({
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const slot = slotTier === 'main' ? mainSlot : secondarySlots[0];
|
// Note: In a real app, we would fetch the raw price from the API or a ViewModel
|
||||||
const slotPrice = slot?.price ?? 0;
|
// For now, we assume the parent handles the actual request logic
|
||||||
|
|
||||||
const request = {
|
|
||||||
sponsorId: currentSponsorId,
|
|
||||||
entityType: getSponsorableEntityType(entityType),
|
|
||||||
entityId,
|
|
||||||
tier: slotTier,
|
|
||||||
offeredAmount: slotPrice * 100,
|
|
||||||
currency: (slot?.currency as 'USD' | 'EUR' | 'GBP') ?? 'USD',
|
|
||||||
message: `Interested in sponsoring ${entityName} as ${slotTier} sponsor.`,
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Sponsorship request:', request);
|
|
||||||
setAppliedTiers(prev => new Set([...prev, slotTier]));
|
|
||||||
onSponsorshipRequested?.(slotTier);
|
onSponsorshipRequested?.(slotTier);
|
||||||
|
setAppliedTiers(prev => new Set([...prev, slotTier]));
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to apply for sponsorship:', err);
|
console.error('Failed to apply for sponsorship:', err);
|
||||||
@@ -134,7 +122,7 @@ export function SponsorInsightsCard({
|
|||||||
} finally {
|
} finally {
|
||||||
setApplyingTier(null);
|
setApplyingTier(null);
|
||||||
}
|
}
|
||||||
}, [currentSponsorId, ctaHref, entityType, entityId, entityName, onNavigate, mainSlot, secondarySlots, appliedTiers, getSponsorableEntityType, onSponsorshipRequested]);
|
}, [currentSponsorId, ctaHref, entityType, entityId, onNavigate, appliedTiers, onSponsorshipRequested]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@@ -171,27 +159,27 @@ export function SponsorInsightsCard({
|
|||||||
})}
|
})}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{(trustScore !== undefined || discordMembers !== undefined || monthlyActivity !== undefined) && (
|
{(trustScoreLabel || discordMembersLabel || monthlyActivityLabel) && (
|
||||||
<Stack display="flex" flexWrap="wrap" gap={4} mb={4} pb={4} borderBottom borderColor="border-charcoal-outline/50">
|
<Stack display="flex" flexWrap="wrap" gap={4} mb={4} pb={4} borderBottom borderColor="border-charcoal-outline/50">
|
||||||
{trustScore !== undefined && (
|
{trustScoreLabel && (
|
||||||
<Stack direction="row" align="center" gap={2}>
|
<Stack direction="row" align="center" gap={2}>
|
||||||
<Icon icon={Shield} size={4} color="rgb(16, 185, 129)" />
|
<Icon icon={Shield} size={4} color="rgb(16, 185, 129)" />
|
||||||
<Text size="sm" color="text-gray-400">Trust Score:</Text>
|
<Text size="sm" color="text-gray-400">Trust Score:</Text>
|
||||||
<Text size="sm" weight="semibold" color="text-white">{trustScore}/100</Text>
|
<Text size="sm" weight="semibold" color="text-white">{trustScoreLabel}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{discordMembers !== undefined && (
|
{discordMembersLabel && (
|
||||||
<Stack direction="row" align="center" gap={2}>
|
<Stack direction="row" align="center" gap={2}>
|
||||||
<Icon icon={MessageCircle} size={4} color="rgb(168, 85, 247)" />
|
<Icon icon={MessageCircle} size={4} color="rgb(168, 85, 247)" />
|
||||||
<Text size="sm" color="text-gray-400">Discord:</Text>
|
<Text size="sm" color="text-gray-400">Discord:</Text>
|
||||||
<Text size="sm" weight="semibold" color="text-white">{discordMembers.toLocaleString()}</Text>
|
<Text size="sm" weight="semibold" color="text-white">{discordMembersLabel}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{monthlyActivity !== undefined && (
|
{monthlyActivityLabel && (
|
||||||
<Stack direction="row" align="center" gap={2}>
|
<Stack direction="row" align="center" gap={2}>
|
||||||
<Icon icon={Activity} size={4} color="rgb(0, 255, 255)" />
|
<Icon icon={Activity} size={4} color="rgb(0, 255, 255)" />
|
||||||
<Text size="sm" color="text-gray-400">Monthly Activity:</Text>
|
<Text size="sm" color="text-gray-400">Monthly Activity:</Text>
|
||||||
<Text size="sm" weight="semibold" color="text-white">{monthlyActivity}%</Text>
|
<Text size="sm" weight="semibold" color="text-white">{monthlyActivityLabel}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -206,7 +194,7 @@ export function SponsorInsightsCard({
|
|||||||
statusColor={mainSlot.available ? 'text-performance-green' : 'text-gray-500'}
|
statusColor={mainSlot.available ? 'text-performance-green' : 'text-gray-500'}
|
||||||
benefits={mainSlot.benefits.join(' • ')}
|
benefits={mainSlot.benefits.join(' • ')}
|
||||||
available={mainSlot.available}
|
available={mainSlot.available}
|
||||||
price={`$${mainSlot.price.toLocaleString()}/season`}
|
price={mainSlot.priceLabel}
|
||||||
action={
|
action={
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
@@ -240,7 +228,7 @@ export function SponsorInsightsCard({
|
|||||||
statusColor={availableSecondary > 0 ? 'text-purple-400' : 'text-gray-500'}
|
statusColor={availableSecondary > 0 ? 'text-purple-400' : 'text-gray-500'}
|
||||||
benefits={secondarySlots[0]?.benefits.join(' • ') || 'Logo placement on page'}
|
benefits={secondarySlots[0]?.benefits.join(' • ') || 'Logo placement on page'}
|
||||||
available={availableSecondary > 0}
|
available={availableSecondary > 0}
|
||||||
price={`$${secondarySlots[0]?.price.toLocaleString()}/season`}
|
price={secondarySlots[0]?.priceLabel}
|
||||||
action={
|
action={
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -275,7 +263,7 @@ export function SponsorInsightsCard({
|
|||||||
<Stack key={index} direction="row" align="center" gap={2}>
|
<Stack key={index} direction="row" align="center" gap={2}>
|
||||||
<Text size="sm" color="text-gray-500">{item.label}:</Text>
|
<Text size="sm" color="text-gray-500">{item.label}:</Text>
|
||||||
<Text size="sm" weight="semibold" color="text-white">
|
<Text size="sm" weight="semibold" color="text-white">
|
||||||
{typeof item.value === 'number' ? item.value.toLocaleString() : item.value}
|
{item.value}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { ComponentType } from 'react';
|
|||||||
export interface SponsorMetric {
|
export interface SponsorMetric {
|
||||||
icon: string | ComponentType;
|
icon: string | ComponentType;
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
trend?: {
|
trend?: {
|
||||||
value: number;
|
value: string;
|
||||||
isPositive: boolean;
|
isPositive: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,6 @@ export interface SponsorMetric {
|
|||||||
export interface SponsorshipSlot {
|
export interface SponsorshipSlot {
|
||||||
tier: 'main' | 'secondary';
|
tier: 'main' | 'secondary';
|
||||||
available: boolean;
|
available: boolean;
|
||||||
price: number;
|
priceLabel: string;
|
||||||
currency?: string;
|
|
||||||
benefits: string[];
|
benefits: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { LucideIcon } from 'lucide-react';
|
|||||||
|
|
||||||
interface SponsorMetricCardProps {
|
interface SponsorMetricCardProps {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string;
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
color?: string;
|
color?: string;
|
||||||
trend?: {
|
trend?: {
|
||||||
value: number;
|
value: string;
|
||||||
isPositive: boolean;
|
isPositive: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -35,11 +35,11 @@ export function SponsorMetricCard({
|
|||||||
</Box>
|
</Box>
|
||||||
<Box display="flex" alignItems="baseline" gap={2}>
|
<Box display="flex" alignItems="baseline" gap={2}>
|
||||||
<Text size="xl" weight="bold" color="text-white">
|
<Text size="xl" weight="bold" color="text-white">
|
||||||
{typeof value === 'number' ? value.toLocaleString() : value}
|
{value}
|
||||||
</Text>
|
</Text>
|
||||||
{trend && (
|
{trend && (
|
||||||
<Text size="xs" color={trend.isPositive ? 'text-performance-green' : 'text-red-400'}>
|
<Text size="xs" color={trend.isPositive ? 'text-performance-green' : 'text-red-400'}>
|
||||||
{trend.isPositive ? '+' : ''}{trend.value}%
|
{trend.value}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { LucideIcon } from 'lucide-react';
|
|||||||
interface SponsorshipCategoryCardProps {
|
interface SponsorshipCategoryCardProps {
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
title: string;
|
title: string;
|
||||||
count: number;
|
countLabel: string;
|
||||||
impressions: number;
|
impressionsLabel: string;
|
||||||
color: string;
|
color: string;
|
||||||
href: string;
|
href: string;
|
||||||
}
|
}
|
||||||
@@ -17,8 +17,8 @@ interface SponsorshipCategoryCardProps {
|
|||||||
export function SponsorshipCategoryCard({
|
export function SponsorshipCategoryCard({
|
||||||
icon,
|
icon,
|
||||||
title,
|
title,
|
||||||
count,
|
countLabel,
|
||||||
impressions,
|
impressionsLabel,
|
||||||
color,
|
color,
|
||||||
href
|
href
|
||||||
}: SponsorshipCategoryCardProps) {
|
}: SponsorshipCategoryCardProps) {
|
||||||
@@ -39,11 +39,11 @@ export function SponsorshipCategoryCard({
|
|||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text weight="medium" color="text-white" block>{title}</Text>
|
<Text weight="medium" color="text-white" block>{title}</Text>
|
||||||
<Text size="sm" color="text-gray-500">{count} active</Text>
|
<Text size="sm" color="text-gray-500">{countLabel}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack textAlign="right">
|
<Stack textAlign="right">
|
||||||
<Text weight="semibold" color="text-white" block>{impressions.toLocaleString()}</Text>
|
<Text weight="semibold" color="text-white" block>{impressionsLabel}</Text>
|
||||||
<Text size="xs" color="text-gray-500">impressions</Text>
|
<Text size="xs" color="text-gray-500">impressions</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import { AlertTriangle, Check, Clock, Download, Receipt } from 'lucide-react';
|
|||||||
|
|
||||||
export interface Transaction {
|
export interface Transaction {
|
||||||
id: string;
|
id: string;
|
||||||
date: string;
|
formattedDate: string;
|
||||||
description: string;
|
description: string;
|
||||||
amount: number;
|
formattedAmount: string;
|
||||||
status: 'paid' | 'pending' | 'overdue' | 'failed';
|
status: 'paid' | 'pending' | 'overdue' | 'failed';
|
||||||
invoiceNumber: string;
|
invoiceNumber: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -103,11 +103,11 @@ export function TransactionTable({ transactions, onDownload }: TransactionTableP
|
|||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack colSpan={{ base: 1, md: 2 } as any}>
|
<Stack colSpan={{ base: 1, md: 2 } as any}>
|
||||||
<Text size="sm" color="text-gray-400">{new Date(tx.date).toLocaleDateString()}</Text>
|
<Text size="sm" color="text-gray-400">{tx.formattedDate}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack colSpan={{ base: 1, md: 2 } as any}>
|
<Stack colSpan={{ base: 1, md: 2 } as any}>
|
||||||
<Text weight="semibold" color="text-white">${tx.amount.toFixed(2)}</Text>
|
<Text weight="semibold" color="text-white">{tx.formattedAmount}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack colSpan={{ base: 1, md: 2 } as any}>
|
<Stack colSpan={{ base: 1, md: 2 } as any}>
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ interface SkillLevelSectionProps {
|
|||||||
description?: string;
|
description?: string;
|
||||||
logoUrl?: string;
|
logoUrl?: string;
|
||||||
memberCount: number;
|
memberCount: number;
|
||||||
rating?: number;
|
ratingLabel: string;
|
||||||
totalWins: number;
|
winsLabel: string;
|
||||||
totalRaces: number;
|
racesLabel: string;
|
||||||
performanceLevel: string;
|
performanceLevel: string;
|
||||||
isRecruiting: boolean;
|
isRecruiting: boolean;
|
||||||
specialization?: string;
|
specialization?: string;
|
||||||
@@ -86,9 +86,9 @@ export function SkillLevelSection({
|
|||||||
description={team.description ?? ''}
|
description={team.description ?? ''}
|
||||||
logo={team.logoUrl}
|
logo={team.logoUrl}
|
||||||
memberCount={team.memberCount}
|
memberCount={team.memberCount}
|
||||||
rating={team.rating}
|
ratingLabel={team.ratingLabel}
|
||||||
totalWins={team.totalWins}
|
winsLabel={team.winsLabel}
|
||||||
totalRaces={team.totalRaces}
|
racesLabel={team.racesLabel}
|
||||||
performanceLevel={team.performanceLevel as SkillLevel}
|
performanceLevel={team.performanceLevel as SkillLevel}
|
||||||
isRecruiting={team.isRecruiting}
|
isRecruiting={team.isRecruiting}
|
||||||
specialization={specialization(team.specialization)}
|
specialization={specialization(team.specialization)}
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ interface TeamCardProps {
|
|||||||
description?: string;
|
description?: string;
|
||||||
logo?: string;
|
logo?: string;
|
||||||
memberCount: number;
|
memberCount: number;
|
||||||
rating?: number | null;
|
ratingLabel: string;
|
||||||
totalWins?: number;
|
winsLabel: string;
|
||||||
totalRaces?: number;
|
racesLabel: string;
|
||||||
performanceLevel?: 'beginner' | 'intermediate' | 'advanced' | 'pro';
|
performanceLevel?: 'beginner' | 'intermediate' | 'advanced' | 'pro';
|
||||||
isRecruiting?: boolean;
|
isRecruiting?: boolean;
|
||||||
specialization?: 'endurance' | 'sprint' | 'mixed' | undefined;
|
specialization?: 'endurance' | 'sprint' | 'mixed' | undefined;
|
||||||
@@ -65,9 +65,9 @@ export function TeamCard({
|
|||||||
description,
|
description,
|
||||||
logo,
|
logo,
|
||||||
memberCount,
|
memberCount,
|
||||||
rating,
|
ratingLabel,
|
||||||
totalWins,
|
winsLabel,
|
||||||
totalRaces,
|
racesLabel,
|
||||||
performanceLevel,
|
performanceLevel,
|
||||||
isRecruiting,
|
isRecruiting,
|
||||||
specialization,
|
specialization,
|
||||||
@@ -112,9 +112,9 @@ export function TeamCard({
|
|||||||
)}
|
)}
|
||||||
statsContent={
|
statsContent={
|
||||||
<Group gap={4} justify="center">
|
<Group gap={4} justify="center">
|
||||||
<TeamStatItem label="Rating" value={typeof rating === 'number' ? Math.round(rating).toLocaleString() : '—'} intent="primary" align="center" />
|
<TeamStatItem label="Rating" value={ratingLabel} intent="primary" align="center" />
|
||||||
<TeamStatItem label="Wins" value={totalWins ?? 0} intent="success" align="center" />
|
<TeamStatItem label="Wins" value={winsLabel} intent="success" align="center" />
|
||||||
<TeamStatItem label="Races" value={totalRaces ?? 0} intent="high" align="center" />
|
<TeamStatItem label="Races" value={racesLabel} intent="high" align="center" />
|
||||||
</Group>
|
</Group>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ interface TeamLeaderboardItemProps {
|
|||||||
name: string;
|
name: string;
|
||||||
logoUrl: string;
|
logoUrl: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
memberCount: number;
|
memberCountLabel: string;
|
||||||
totalWins: number;
|
totalWinsLabel: string;
|
||||||
isRecruiting: boolean;
|
isRecruiting: boolean;
|
||||||
rating?: number;
|
ratingLabel: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
medalColor: string;
|
medalColor: string;
|
||||||
medalBg: string;
|
medalBg: string;
|
||||||
@@ -26,10 +26,10 @@ export function TeamLeaderboardItem({
|
|||||||
name,
|
name,
|
||||||
logoUrl,
|
logoUrl,
|
||||||
category,
|
category,
|
||||||
memberCount,
|
memberCountLabel,
|
||||||
totalWins,
|
totalWinsLabel,
|
||||||
isRecruiting,
|
isRecruiting,
|
||||||
rating,
|
ratingLabel,
|
||||||
onClick,
|
onClick,
|
||||||
medalColor,
|
medalColor,
|
||||||
medalBg,
|
medalBg,
|
||||||
@@ -99,11 +99,11 @@ export function TeamLeaderboardItem({
|
|||||||
)}
|
)}
|
||||||
<Stack direction="row" align="center" gap={1}>
|
<Stack direction="row" align="center" gap={1}>
|
||||||
<Icon icon={Users} size={3} color="var(--text-gray-600)" />
|
<Icon icon={Users} size={3} color="var(--text-gray-600)" />
|
||||||
<Text size="xs" color="text-gray-500">{memberCount}</Text>
|
<Text size="xs" color="text-gray-500">{memberCountLabel}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction="row" align="center" gap={1}>
|
<Stack direction="row" align="center" gap={1}>
|
||||||
<Icon icon={Trophy} size={3} color="var(--text-gray-600)" />
|
<Icon icon={Trophy} size={3} color="var(--text-gray-600)" />
|
||||||
<Text size="xs" color="text-gray-500">{totalWins} wins</Text>
|
<Text size="xs" color="text-gray-500">{totalWinsLabel}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
{isRecruiting && (
|
{isRecruiting && (
|
||||||
<Stack direction="row" align="center" gap={1}>
|
<Stack direction="row" align="center" gap={1}>
|
||||||
@@ -117,7 +117,7 @@ export function TeamLeaderboardItem({
|
|||||||
{/* Rating */}
|
{/* Rating */}
|
||||||
<Stack textAlign="right">
|
<Stack textAlign="right">
|
||||||
<Text font="mono" weight="semibold" color="text-purple-400" block>
|
<Text font="mono" weight="semibold" color="text-purple-400" block>
|
||||||
{typeof rating === 'number' ? Math.round(rating).toLocaleString() : '—'}
|
{ratingLabel}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs" color="text-gray-500">Rating</Text>
|
<Text size="xs" color="text-gray-500">Rating</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ interface TeamLeaderboardPanelProps {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
logoUrl?: string;
|
logoUrl?: string;
|
||||||
rating: number;
|
ratingLabel: string;
|
||||||
wins: number;
|
winsLabel: string;
|
||||||
races: number;
|
racesLabel: string;
|
||||||
memberCount: number;
|
memberCountLabel: string;
|
||||||
}>;
|
}>;
|
||||||
onTeamClick: (id: string) => void;
|
onTeamClick: (id: string) => void;
|
||||||
}
|
}
|
||||||
@@ -53,16 +53,16 @@ export function TeamLeaderboardPanel({ teams, onTeamClick }: TeamLeaderboardPane
|
|||||||
</Stack>
|
</Stack>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<Text font="mono" weight="bold" color="text-primary-blue">{team.rating}</Text>
|
<Text font="mono" weight="bold" color="text-primary-blue">{team.ratingLabel}</Text>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<Text font="mono" color="text-gray-300">{team.wins}</Text>
|
<Text font="mono" color="text-gray-300">{team.winsLabel}</Text>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<Text font="mono" color="text-gray-300">{team.races}</Text>
|
<Text font="mono" color="text-gray-300">{team.racesLabel}</Text>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<Text font="mono" color="text-gray-400" size="xs">{team.memberCount}</Text>
|
<Text font="mono" color="text-gray-400" size="xs">{team.memberCountLabel}</Text>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ import { ChevronRight, Users } from 'lucide-react';
|
|||||||
interface TeamMembershipCardProps {
|
interface TeamMembershipCardProps {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
role: string;
|
role: string;
|
||||||
joinedAt: string;
|
joinedAtLabel: string;
|
||||||
href: string;
|
href: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TeamMembershipCard({
|
export function TeamMembershipCard({
|
||||||
teamName,
|
teamName,
|
||||||
role,
|
role,
|
||||||
joinedAt,
|
joinedAtLabel,
|
||||||
href,
|
href,
|
||||||
}: TeamMembershipCardProps) {
|
}: TeamMembershipCardProps) {
|
||||||
return (
|
return (
|
||||||
@@ -52,7 +52,7 @@ export function TeamMembershipCard({
|
|||||||
{role}
|
{role}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Text size="xs" color="text-gray-400">
|
<Text size="xs" color="text-gray-400">
|
||||||
Since {new Date(joinedAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
|
Since {joinedAtLabel}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface TeamMembership {
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
role: string;
|
role: string;
|
||||||
joinedAt: Date;
|
joinedAtLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TeamMembershipGridProps {
|
interface TeamMembershipGridProps {
|
||||||
@@ -46,7 +46,7 @@ export function TeamMembershipGrid({ memberships }: TeamMembershipGridProps) {
|
|||||||
<Surface variant="muted" rounded="full" padding={1} style={{ paddingLeft: '0.5rem', paddingRight: '0.5rem', backgroundColor: 'rgba(147, 51, 234, 0.2)', color: '#a855f7' }}>
|
<Surface variant="muted" rounded="full" padding={1} style={{ paddingLeft: '0.5rem', paddingRight: '0.5rem', backgroundColor: 'rgba(147, 51, 234, 0.2)', color: '#a855f7' }}>
|
||||||
<Text size="xs" weight="medium" style={{ textTransform: 'capitalize' }}>{membership.role}</Text>
|
<Text size="xs" weight="medium" style={{ textTransform: 'capitalize' }}>{membership.role}</Text>
|
||||||
</Surface>
|
</Surface>
|
||||||
<Text size="xs" color="text-gray-500">Since {membership.joinedAt.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}</Text>
|
<Text size="xs" color="text-gray-400">Since {membership.joinedAtLabel}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<ChevronRight style={{ width: '1rem', height: '1rem', color: '#737373' }} />
|
<ChevronRight style={{ width: '1rem', height: '1rem', color: '#737373' }} />
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ import { Select } from '@/ui/Select';
|
|||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { MemberDisplay } from '@/lib/display-objects/MemberDisplay';
|
||||||
|
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
|
||||||
|
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
||||||
|
|
||||||
export type TeamRole = 'owner' | 'admin' | 'member';
|
export type TeamRole = 'owner' | 'admin' | 'member';
|
||||||
export type TeamMemberRole = 'owner' | 'manager' | 'member';
|
export type TeamMemberRole = 'owner' | 'manager' | 'member';
|
||||||
|
|
||||||
@@ -67,9 +71,10 @@ export function TeamRoster({
|
|||||||
return sortMembers(teamMembers as unknown as TeamMember[], sortBy);
|
return sortMembers(teamMembers as unknown as TeamMember[], sortBy);
|
||||||
}, [teamMembers, sortBy]);
|
}, [teamMembers, sortBy]);
|
||||||
|
|
||||||
const teamAverageRating = useMemo(() => {
|
const teamAverageRatingLabel = useMemo(() => {
|
||||||
if (teamMembers.length === 0) return 0;
|
if (teamMembers.length === 0) return '—';
|
||||||
return teamMembers.reduce((sum: number, m: { rating?: number | null }) => sum + (m.rating || 0), 0) / teamMembers.length;
|
const avg = teamMembers.reduce((sum: number, m: { rating?: number | null }) => sum + (m.rating || 0), 0) / teamMembers.length;
|
||||||
|
return RatingDisplay.format(avg);
|
||||||
}, [teamMembers]);
|
}, [teamMembers]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -88,8 +93,8 @@ export function TeamRoster({
|
|||||||
<Stack>
|
<Stack>
|
||||||
<Heading level={3}>Team Roster</Heading>
|
<Heading level={3}>Team Roster</Heading>
|
||||||
<Text size="sm" color="text-gray-400" block mt={1}>
|
<Text size="sm" color="text-gray-400" block mt={1}>
|
||||||
{memberships.length} {memberships.length === 1 ? 'member' : 'members'} • Avg Rating:{' '}
|
{MemberDisplay.formatCount(memberships.length)} • Avg Rating:{' '}
|
||||||
<Text color="text-primary-blue" weight="medium">{teamAverageRating.toFixed(0)}</Text>
|
<Text color="text-primary-blue" weight="medium">{teamAverageRatingLabel}</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@@ -124,9 +129,9 @@ export function TeamRoster({
|
|||||||
driver={driver as DriverViewModel}
|
driver={driver as DriverViewModel}
|
||||||
href={`${routes.driver.detail(driver.id)}?from=team&teamId=${teamId}`}
|
href={`${routes.driver.detail(driver.id)}?from=team&teamId=${teamId}`}
|
||||||
roleLabel={getRoleLabel(role)}
|
roleLabel={getRoleLabel(role)}
|
||||||
joinedAt={joinedAt}
|
joinedAtLabel={DateDisplay.formatShort(joinedAt)}
|
||||||
rating={rating}
|
ratingLabel={RatingDisplay.format(rating)}
|
||||||
overallRank={overallRank}
|
overallRankLabel={overallRank !== null ? `#${overallRank}` : null}
|
||||||
actions={canManageMembership ? (
|
actions={canManageMembership ? (
|
||||||
<>
|
<>
|
||||||
<Stack width="32">
|
<Stack width="32">
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ interface TeamRosterItemProps {
|
|||||||
driver: DriverViewModel;
|
driver: DriverViewModel;
|
||||||
href: string;
|
href: string;
|
||||||
roleLabel: string;
|
roleLabel: string;
|
||||||
joinedAt: string | Date;
|
joinedAtLabel: string;
|
||||||
rating: number | null;
|
ratingLabel: string | null;
|
||||||
overallRank: number | null;
|
overallRankLabel: string | null;
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,9 +18,9 @@ export function TeamRosterItem({
|
|||||||
driver,
|
driver,
|
||||||
href,
|
href,
|
||||||
roleLabel,
|
roleLabel,
|
||||||
joinedAt,
|
joinedAtLabel,
|
||||||
rating,
|
ratingLabel,
|
||||||
overallRank,
|
overallRankLabel,
|
||||||
actions,
|
actions,
|
||||||
}: TeamRosterItemProps) {
|
}: TeamRosterItemProps) {
|
||||||
return (
|
return (
|
||||||
@@ -38,23 +38,23 @@ export function TeamRosterItem({
|
|||||||
contextLabel={roleLabel}
|
contextLabel={roleLabel}
|
||||||
meta={
|
meta={
|
||||||
<Text size="xs" color="text-gray-400">
|
<Text size="xs" color="text-gray-400">
|
||||||
{driver.country} • Joined {new Date(joinedAt).toLocaleDateString()}
|
{driver.country} • Joined {joinedAtLabel}
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{rating !== null && (
|
{ratingLabel !== null && (
|
||||||
<Stack direction="row" align="center" gap={6}>
|
<Stack direction="row" align="center" gap={6}>
|
||||||
<Stack display="flex" flexDirection="col" alignItems="center">
|
<Stack display="flex" flexDirection="col" alignItems="center">
|
||||||
<Text size="lg" weight="bold" color="text-primary-blue" block>
|
<Text size="lg" weight="bold" color="text-primary-blue" block>
|
||||||
{rating}
|
{ratingLabel}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs" color="text-gray-400">Rating</Text>
|
<Text size="xs" color="text-gray-400">Rating</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
{overallRank !== null && (
|
{overallRankLabel !== null && (
|
||||||
<Stack display="flex" flexDirection="col" alignItems="center">
|
<Stack display="flex" flexDirection="col" alignItems="center">
|
||||||
<Text size="sm" color="text-gray-300" block>#{overallRank}</Text>
|
<Text size="sm" color="text-gray-300" block>{overallRankLabel}</Text>
|
||||||
<Text size="xs" color="text-gray-500">Rank</Text>
|
<Text size="xs" color="text-gray-500">Rank</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
|
|||||||
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
|
||||||
import { LeagueScheduleViewModel, LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueScheduleViewModel';
|
import { LeagueScheduleViewModel, LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueScheduleViewModel';
|
||||||
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
|
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
|
||||||
|
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
||||||
|
|
||||||
function mapRaceDtoToViewModel(race: RaceDTO): LeagueScheduleRaceViewModel {
|
function mapRaceDtoToViewModel(race: RaceDTO): LeagueScheduleRaceViewModel {
|
||||||
const scheduledAt = race.date ? new Date(race.date) : new Date(0);
|
const scheduledAt = race.date ? new Date(race.date) : new Date(0);
|
||||||
@@ -14,6 +15,8 @@ function mapRaceDtoToViewModel(race: RaceDTO): LeagueScheduleRaceViewModel {
|
|||||||
id: race.id,
|
id: race.id,
|
||||||
name: race.name,
|
name: race.name,
|
||||||
scheduledAt,
|
scheduledAt,
|
||||||
|
formattedDate: DateDisplay.formatShort(scheduledAt),
|
||||||
|
formattedTime: DateDisplay.formatTime(scheduledAt),
|
||||||
isPast,
|
isPast,
|
||||||
isUpcoming: !isPast,
|
isUpcoming: !isPast,
|
||||||
status: isPast ? 'completed' : 'scheduled',
|
status: isPast ? 'completed' : 'scheduled',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueScheduleVie
|
|||||||
import { LeagueSeasonSummaryViewModel } from '@/lib/view-models/LeagueSeasonSummaryViewModel';
|
import { LeagueSeasonSummaryViewModel } from '@/lib/view-models/LeagueSeasonSummaryViewModel';
|
||||||
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
|
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
|
||||||
import type { LeagueSeasonSummaryDTO } from '@/lib/types/generated/LeagueSeasonSummaryDTO';
|
import type { LeagueSeasonSummaryDTO } from '@/lib/types/generated/LeagueSeasonSummaryDTO';
|
||||||
|
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
||||||
|
|
||||||
function mapRaceDtoToViewModel(race: RaceDTO): LeagueScheduleRaceViewModel {
|
function mapRaceDtoToViewModel(race: RaceDTO): LeagueScheduleRaceViewModel {
|
||||||
const scheduledAt = race.date ? new Date(race.date) : new Date(0);
|
const scheduledAt = race.date ? new Date(race.date) : new Date(0);
|
||||||
@@ -17,6 +18,8 @@ function mapRaceDtoToViewModel(race: RaceDTO): LeagueScheduleRaceViewModel {
|
|||||||
id: race.id,
|
id: race.id,
|
||||||
name: race.name,
|
name: race.name,
|
||||||
scheduledAt,
|
scheduledAt,
|
||||||
|
formattedDate: DateDisplay.formatShort(scheduledAt),
|
||||||
|
formattedTime: DateDisplay.formatTime(scheduledAt),
|
||||||
isPast,
|
isPast,
|
||||||
isUpcoming: !isPast,
|
isUpcoming: !isPast,
|
||||||
status: isPast ? 'completed' : 'scheduled',
|
status: isPast ? 'completed' : 'scheduled',
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
|
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
|
||||||
import type { DriverProfileViewData } from '@/lib/types/view-data/DriverProfileViewData';
|
import type { DriverProfileViewData } from '@/lib/types/view-data/DriverProfileViewData';
|
||||||
|
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
||||||
|
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
|
||||||
|
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
|
||||||
|
import { FinishDisplay } from '@/lib/display-objects/FinishDisplay';
|
||||||
|
import { PercentDisplay } from '@/lib/display-objects/PercentDisplay';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DriverProfileViewDataBuilder
|
* DriverProfileViewDataBuilder
|
||||||
@@ -17,26 +22,38 @@ export class DriverProfileViewDataBuilder {
|
|||||||
avatarUrl: apiDto.currentDriver.avatarUrl || '',
|
avatarUrl: apiDto.currentDriver.avatarUrl || '',
|
||||||
iracingId: typeof apiDto.currentDriver.iracingId === 'string' ? parseInt(apiDto.currentDriver.iracingId, 10) : (apiDto.currentDriver.iracingId ?? null),
|
iracingId: typeof apiDto.currentDriver.iracingId === 'string' ? parseInt(apiDto.currentDriver.iracingId, 10) : (apiDto.currentDriver.iracingId ?? null),
|
||||||
joinedAt: apiDto.currentDriver.joinedAt,
|
joinedAt: apiDto.currentDriver.joinedAt,
|
||||||
|
joinedAtLabel: DateDisplay.formatMonthYear(apiDto.currentDriver.joinedAt),
|
||||||
rating: apiDto.currentDriver.rating ?? null,
|
rating: apiDto.currentDriver.rating ?? null,
|
||||||
|
ratingLabel: RatingDisplay.format(apiDto.currentDriver.rating),
|
||||||
globalRank: apiDto.currentDriver.globalRank ?? null,
|
globalRank: apiDto.currentDriver.globalRank ?? null,
|
||||||
|
globalRankLabel: apiDto.currentDriver.globalRank != null ? `#${apiDto.currentDriver.globalRank}` : '—',
|
||||||
consistency: apiDto.currentDriver.consistency ?? null,
|
consistency: apiDto.currentDriver.consistency ?? null,
|
||||||
bio: apiDto.currentDriver.bio ?? null,
|
bio: apiDto.currentDriver.bio ?? null,
|
||||||
totalDrivers: apiDto.currentDriver.totalDrivers ?? null,
|
totalDrivers: apiDto.currentDriver.totalDrivers ?? null,
|
||||||
} : null,
|
} : null,
|
||||||
stats: apiDto.stats ? {
|
stats: apiDto.stats ? {
|
||||||
totalRaces: apiDto.stats.totalRaces,
|
totalRaces: apiDto.stats.totalRaces,
|
||||||
|
totalRacesLabel: NumberDisplay.format(apiDto.stats.totalRaces),
|
||||||
wins: apiDto.stats.wins,
|
wins: apiDto.stats.wins,
|
||||||
|
winsLabel: NumberDisplay.format(apiDto.stats.wins),
|
||||||
podiums: apiDto.stats.podiums,
|
podiums: apiDto.stats.podiums,
|
||||||
|
podiumsLabel: NumberDisplay.format(apiDto.stats.podiums),
|
||||||
dnfs: apiDto.stats.dnfs,
|
dnfs: apiDto.stats.dnfs,
|
||||||
|
dnfsLabel: NumberDisplay.format(apiDto.stats.dnfs),
|
||||||
avgFinish: apiDto.stats.avgFinish ?? null,
|
avgFinish: apiDto.stats.avgFinish ?? null,
|
||||||
|
avgFinishLabel: FinishDisplay.formatAverage(apiDto.stats.avgFinish),
|
||||||
bestFinish: apiDto.stats.bestFinish ?? null,
|
bestFinish: apiDto.stats.bestFinish ?? null,
|
||||||
|
bestFinishLabel: FinishDisplay.format(apiDto.stats.bestFinish),
|
||||||
worstFinish: apiDto.stats.worstFinish ?? null,
|
worstFinish: apiDto.stats.worstFinish ?? null,
|
||||||
|
worstFinishLabel: FinishDisplay.format(apiDto.stats.worstFinish),
|
||||||
finishRate: apiDto.stats.finishRate ?? null,
|
finishRate: apiDto.stats.finishRate ?? null,
|
||||||
winRate: apiDto.stats.winRate ?? null,
|
winRate: apiDto.stats.winRate ?? null,
|
||||||
podiumRate: apiDto.stats.podiumRate ?? null,
|
podiumRate: apiDto.stats.podiumRate ?? null,
|
||||||
percentile: apiDto.stats.percentile ?? null,
|
percentile: apiDto.stats.percentile ?? null,
|
||||||
rating: apiDto.stats.rating ?? null,
|
rating: apiDto.stats.rating ?? null,
|
||||||
|
ratingLabel: RatingDisplay.format(apiDto.stats.rating),
|
||||||
consistency: apiDto.stats.consistency ?? null,
|
consistency: apiDto.stats.consistency ?? null,
|
||||||
|
consistencyLabel: PercentDisplay.formatWhole(apiDto.stats.consistency),
|
||||||
overallRank: apiDto.stats.overallRank ?? null,
|
overallRank: apiDto.stats.overallRank ?? null,
|
||||||
} : null,
|
} : null,
|
||||||
finishDistribution: apiDto.finishDistribution ? {
|
finishDistribution: apiDto.finishDistribution ? {
|
||||||
@@ -53,6 +70,7 @@ export class DriverProfileViewDataBuilder {
|
|||||||
teamTag: m.teamTag ?? null,
|
teamTag: m.teamTag ?? null,
|
||||||
role: m.role,
|
role: m.role,
|
||||||
joinedAt: m.joinedAt,
|
joinedAt: m.joinedAt,
|
||||||
|
joinedAtLabel: DateDisplay.formatMonthYear(m.joinedAt),
|
||||||
isCurrent: m.isCurrent,
|
isCurrent: m.isCurrent,
|
||||||
})),
|
})),
|
||||||
socialSummary: {
|
socialSummary: {
|
||||||
@@ -76,7 +94,9 @@ export class DriverProfileViewDataBuilder {
|
|||||||
description: a.description,
|
description: a.description,
|
||||||
icon: a.icon,
|
icon: a.icon,
|
||||||
rarity: a.rarity,
|
rarity: a.rarity,
|
||||||
|
rarityLabel: a.rarity,
|
||||||
earnedAt: a.earnedAt,
|
earnedAt: a.earnedAt,
|
||||||
|
earnedAtLabel: DateDisplay.formatShort(a.earnedAt),
|
||||||
})),
|
})),
|
||||||
racingStyle: apiDto.extendedProfile.racingStyle,
|
racingStyle: apiDto.extendedProfile.racingStyle,
|
||||||
favoriteTrack: apiDto.extendedProfile.favoriteTrack,
|
favoriteTrack: apiDto.extendedProfile.favoriteTrack,
|
||||||
@@ -88,4 +108,4 @@ export class DriverProfileViewDataBuilder {
|
|||||||
} : null,
|
} : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
|
import type { DriversLeaderboardDTO } from '@/lib/types/generated/DriversLeaderboardDTO';
|
||||||
import type { DriversViewData } from '@/lib/types/view-data/DriversViewData';
|
import type { DriversViewData } from '@/lib/types/view-data/DriversViewData';
|
||||||
|
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
|
||||||
|
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
|
||||||
|
|
||||||
export class DriversViewDataBuilder {
|
export class DriversViewDataBuilder {
|
||||||
static build(dto: DriversLeaderboardDTO): DriversViewData {
|
static build(dto: DriversLeaderboardDTO): DriversViewData {
|
||||||
@@ -8,6 +10,7 @@ export class DriversViewDataBuilder {
|
|||||||
id: driver.id,
|
id: driver.id,
|
||||||
name: driver.name,
|
name: driver.name,
|
||||||
rating: driver.rating,
|
rating: driver.rating,
|
||||||
|
ratingLabel: RatingDisplay.format(driver.rating),
|
||||||
skillLevel: driver.skillLevel,
|
skillLevel: driver.skillLevel,
|
||||||
category: driver.category,
|
category: driver.category,
|
||||||
nationality: driver.nationality,
|
nationality: driver.nationality,
|
||||||
@@ -19,8 +22,12 @@ export class DriversViewDataBuilder {
|
|||||||
avatarUrl: driver.avatarUrl,
|
avatarUrl: driver.avatarUrl,
|
||||||
})),
|
})),
|
||||||
totalRaces: dto.totalRaces,
|
totalRaces: dto.totalRaces,
|
||||||
|
totalRacesLabel: NumberDisplay.format(dto.totalRaces),
|
||||||
totalWins: dto.totalWins,
|
totalWins: dto.totalWins,
|
||||||
|
totalWinsLabel: NumberDisplay.format(dto.totalWins),
|
||||||
activeCount: dto.activeCount,
|
activeCount: dto.activeCount,
|
||||||
|
activeCountLabel: NumberDisplay.format(dto.activeCount),
|
||||||
|
totalDriversLabel: NumberDisplay.format(dto.drivers.length),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
|
import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO';
|
||||||
import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO';
|
import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRosterJoinRequestDTO';
|
||||||
import type { LeagueRosterAdminViewData, RosterMemberData, JoinRequestData } from '@/lib/view-data/LeagueRosterAdminViewData';
|
import type { LeagueRosterAdminViewData, RosterMemberData, JoinRequestData } from '@/lib/view-data/LeagueRosterAdminViewData';
|
||||||
|
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LeagueRosterAdminViewDataBuilder
|
* LeagueRosterAdminViewDataBuilder
|
||||||
@@ -25,6 +26,7 @@ export class LeagueRosterAdminViewDataBuilder {
|
|||||||
},
|
},
|
||||||
role: member.role,
|
role: member.role,
|
||||||
joinedAt: member.joinedAt,
|
joinedAt: member.joinedAt,
|
||||||
|
formattedJoinedAt: DateDisplay.formatShort(member.joinedAt),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Transform join requests
|
// Transform join requests
|
||||||
@@ -35,6 +37,7 @@ export class LeagueRosterAdminViewDataBuilder {
|
|||||||
name: 'Unknown Driver', // driver field is unknown type
|
name: 'Unknown Driver', // driver field is unknown type
|
||||||
},
|
},
|
||||||
requestedAt: req.requestedAt,
|
requestedAt: req.requestedAt,
|
||||||
|
formattedRequestedAt: DateDisplay.formatShort(req.requestedAt),
|
||||||
message: req.message,
|
message: req.message,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { LeagueSponsorshipsViewData } from '@/lib/view-data/leagues/LeagueSponsorshipsViewData';
|
import { LeagueSponsorshipsViewData } from '@/lib/view-data/leagues/LeagueSponsorshipsViewData';
|
||||||
import { LeagueSponsorshipsApiDto } from '@/lib/types/tbd/LeagueSponsorshipsApiDto';
|
import { LeagueSponsorshipsApiDto } from '@/lib/types/tbd/LeagueSponsorshipsApiDto';
|
||||||
|
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
||||||
|
import { StatusDisplay } from '@/lib/display-objects/StatusDisplay';
|
||||||
|
|
||||||
export class LeagueSponsorshipsViewDataBuilder {
|
export class LeagueSponsorshipsViewDataBuilder {
|
||||||
static build(apiDto: LeagueSponsorshipsApiDto): LeagueSponsorshipsViewData {
|
static build(apiDto: LeagueSponsorshipsApiDto): LeagueSponsorshipsViewData {
|
||||||
@@ -9,7 +11,11 @@ export class LeagueSponsorshipsViewDataBuilder {
|
|||||||
onTabChange: () => {},
|
onTabChange: () => {},
|
||||||
league: apiDto.league,
|
league: apiDto.league,
|
||||||
sponsorshipSlots: apiDto.sponsorshipSlots,
|
sponsorshipSlots: apiDto.sponsorshipSlots,
|
||||||
sponsorshipRequests: apiDto.sponsorshipRequests,
|
sponsorshipRequests: apiDto.sponsorshipRequests.map(r => ({
|
||||||
|
...r,
|
||||||
|
formattedRequestedAt: DateDisplay.formatShort(r.requestedAt),
|
||||||
|
statusLabel: StatusDisplay.protestStatus(r.status), // Reusing protest status for now
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { LeagueWalletViewData, LeagueWalletTransactionViewData } from '@/lib/view-data/leagues/LeagueWalletViewData';
|
import { LeagueWalletViewData, LeagueWalletTransactionViewData } from '@/lib/view-data/leagues/LeagueWalletViewData';
|
||||||
import { LeagueWalletApiDto } from '@/lib/types/tbd/LeagueWalletApiDto';
|
import { LeagueWalletApiDto } from '@/lib/types/tbd/LeagueWalletApiDto';
|
||||||
|
import { CurrencyDisplay } from '@/lib/display-objects/CurrencyDisplay';
|
||||||
|
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
||||||
|
|
||||||
export class LeagueWalletViewDataBuilder {
|
export class LeagueWalletViewDataBuilder {
|
||||||
static build(apiDto: LeagueWalletApiDto): LeagueWalletViewData {
|
static build(apiDto: LeagueWalletApiDto): LeagueWalletViewData {
|
||||||
const transactions: LeagueWalletTransactionViewData[] = apiDto.transactions.map(t => ({
|
const transactions: LeagueWalletTransactionViewData[] = apiDto.transactions.map(t => ({
|
||||||
...t,
|
...t,
|
||||||
formattedAmount: `${t.amount} ${apiDto.currency}`,
|
formattedAmount: CurrencyDisplay.format(t.amount, apiDto.currency),
|
||||||
amountColor: t.amount >= 0 ? 'green' : 'red',
|
amountColor: t.amount >= 0 ? 'green' : 'red',
|
||||||
formattedDate: new Date(t.createdAt).toLocaleDateString(),
|
formattedDate: DateDisplay.formatShort(t.createdAt),
|
||||||
statusColor: t.status === 'completed' ? 'green' : t.status === 'pending' ? 'yellow' : 'red',
|
statusColor: t.status === 'completed' ? 'green' : t.status === 'pending' ? 'yellow' : 'red',
|
||||||
typeColor: 'blue',
|
typeColor: 'blue',
|
||||||
}));
|
}));
|
||||||
@@ -15,13 +17,13 @@ export class LeagueWalletViewDataBuilder {
|
|||||||
return {
|
return {
|
||||||
leagueId: apiDto.leagueId,
|
leagueId: apiDto.leagueId,
|
||||||
balance: apiDto.balance,
|
balance: apiDto.balance,
|
||||||
formattedBalance: `${apiDto.balance} ${apiDto.currency}`,
|
formattedBalance: CurrencyDisplay.format(apiDto.balance, apiDto.currency),
|
||||||
totalRevenue: apiDto.balance, // Mock
|
totalRevenue: apiDto.balance, // Mock
|
||||||
formattedTotalRevenue: `${apiDto.balance} ${apiDto.currency}`,
|
formattedTotalRevenue: CurrencyDisplay.format(apiDto.balance, apiDto.currency),
|
||||||
totalFees: 0, // Mock
|
totalFees: 0, // Mock
|
||||||
formattedTotalFees: `0 ${apiDto.currency}`,
|
formattedTotalFees: CurrencyDisplay.format(0, apiDto.currency),
|
||||||
pendingPayouts: 0, // Mock
|
pendingPayouts: 0, // Mock
|
||||||
formattedPendingPayouts: `0 ${apiDto.currency}`,
|
formattedPendingPayouts: CurrencyDisplay.format(0, apiDto.currency),
|
||||||
currency: apiDto.currency,
|
currency: apiDto.currency,
|
||||||
transactions,
|
transactions,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverP
|
|||||||
import type { ProfileViewData } from '@/lib/view-data/ProfileViewData';
|
import type { ProfileViewData } from '@/lib/view-data/ProfileViewData';
|
||||||
import { mediaConfig } from '@/lib/config/mediaConfig';
|
import { mediaConfig } from '@/lib/config/mediaConfig';
|
||||||
import { CountryFlagDisplay } from '@/lib/display-objects/CountryFlagDisplay';
|
import { CountryFlagDisplay } from '@/lib/display-objects/CountryFlagDisplay';
|
||||||
|
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
||||||
|
import { FinishDisplay } from '@/lib/display-objects/FinishDisplay';
|
||||||
|
import { PercentDisplay } from '@/lib/display-objects/PercentDisplay';
|
||||||
|
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
|
||||||
|
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
|
||||||
|
|
||||||
export class ProfileViewDataBuilder {
|
export class ProfileViewDataBuilder {
|
||||||
static build(apiDto: GetDriverProfileOutputDTO): ProfileViewData {
|
static build(apiDto: GetDriverProfileOutputDTO): ProfileViewData {
|
||||||
@@ -29,11 +34,6 @@ export class ProfileViewDataBuilder {
|
|||||||
const socialSummary = apiDto.socialSummary;
|
const socialSummary = apiDto.socialSummary;
|
||||||
const extended = apiDto.extendedProfile ?? null;
|
const extended = apiDto.extendedProfile ?? null;
|
||||||
|
|
||||||
const joinedAtLabel = new Date(driver.joinedAt).toLocaleDateString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
year: 'numeric',
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
driver: {
|
driver: {
|
||||||
id: driver.id,
|
id: driver.id,
|
||||||
@@ -42,22 +42,22 @@ export class ProfileViewDataBuilder {
|
|||||||
countryFlag: CountryFlagDisplay.fromCountryCode(driver.country).toString(),
|
countryFlag: CountryFlagDisplay.fromCountryCode(driver.country).toString(),
|
||||||
avatarUrl: driver.avatarUrl || mediaConfig.avatars.defaultFallback,
|
avatarUrl: driver.avatarUrl || mediaConfig.avatars.defaultFallback,
|
||||||
bio: driver.bio || null,
|
bio: driver.bio || null,
|
||||||
iracingId: driver.iracingId || null,
|
iracingId: driver.iracingId ? String(driver.iracingId) : null,
|
||||||
joinedAtLabel,
|
joinedAtLabel: DateDisplay.formatMonthYear(driver.joinedAt),
|
||||||
},
|
},
|
||||||
stats: stats
|
stats: stats
|
||||||
? {
|
? {
|
||||||
ratingLabel: stats.rating != null ? String(stats.rating) : '0',
|
ratingLabel: RatingDisplay.format(stats.rating),
|
||||||
globalRankLabel: driver.globalRank != null ? `#${driver.globalRank}` : '—',
|
globalRankLabel: driver.globalRank != null ? `#${driver.globalRank}` : '—',
|
||||||
totalRacesLabel: String(stats.totalRaces),
|
totalRacesLabel: NumberDisplay.format(stats.totalRaces),
|
||||||
winsLabel: String(stats.wins),
|
winsLabel: NumberDisplay.format(stats.wins),
|
||||||
podiumsLabel: String(stats.podiums),
|
podiumsLabel: NumberDisplay.format(stats.podiums),
|
||||||
dnfsLabel: String(stats.dnfs),
|
dnfsLabel: NumberDisplay.format(stats.dnfs),
|
||||||
bestFinishLabel: stats.bestFinish != null ? `P${stats.bestFinish}` : '—',
|
bestFinishLabel: FinishDisplay.format(stats.bestFinish),
|
||||||
worstFinishLabel: stats.worstFinish != null ? `P${stats.worstFinish}` : '—',
|
worstFinishLabel: FinishDisplay.format(stats.worstFinish),
|
||||||
avgFinishLabel: stats.avgFinish != null ? `P${stats.avgFinish.toFixed(1)}` : '—',
|
avgFinishLabel: FinishDisplay.formatAverage(stats.avgFinish),
|
||||||
consistencyLabel: stats.consistency != null ? `${stats.consistency}%` : '0%',
|
consistencyLabel: PercentDisplay.formatWhole(stats.consistency),
|
||||||
percentileLabel: stats.percentile != null ? `${stats.percentile}%` : '—',
|
percentileLabel: PercentDisplay.format(stats.percentile),
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
teamMemberships: apiDto.teamMemberships.map((m) => ({
|
teamMemberships: apiDto.teamMemberships.map((m) => ({
|
||||||
@@ -65,10 +65,7 @@ export class ProfileViewDataBuilder {
|
|||||||
teamName: m.teamName,
|
teamName: m.teamName,
|
||||||
teamTag: m.teamTag || null,
|
teamTag: m.teamTag || null,
|
||||||
roleLabel: m.role,
|
roleLabel: m.role,
|
||||||
joinedAtLabel: new Date(m.joinedAt).toLocaleDateString('en-US', {
|
joinedAtLabel: DateDisplay.formatMonthYear(m.joinedAt),
|
||||||
month: 'short',
|
|
||||||
year: 'numeric',
|
|
||||||
}),
|
|
||||||
href: `/teams/${m.teamId}`,
|
href: `/teams/${m.teamId}`,
|
||||||
})),
|
})),
|
||||||
extendedProfile: extended
|
extendedProfile: extended
|
||||||
@@ -89,12 +86,8 @@ export class ProfileViewDataBuilder {
|
|||||||
id: a.id,
|
id: a.id,
|
||||||
title: a.title,
|
title: a.title,
|
||||||
description: a.description,
|
description: a.description,
|
||||||
earnedAtLabel: new Date(a.earnedAt).toLocaleDateString('en-US', {
|
earnedAtLabel: DateDisplay.formatShort(a.earnedAt),
|
||||||
month: 'short',
|
icon: a.icon as any,
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
}),
|
|
||||||
icon: a.icon as NonNullable<ProfileViewData['extendedProfile']>['achievements'][number]['icon'],
|
|
||||||
rarityLabel: a.rarity,
|
rarityLabel: a.rarity,
|
||||||
})),
|
})),
|
||||||
friends: socialSummary.friends.slice(0, 8).map((f) => ({
|
friends: socialSummary.friends.slice(0, 8).map((f) => ({
|
||||||
@@ -104,7 +97,7 @@ export class ProfileViewDataBuilder {
|
|||||||
avatarUrl: f.avatarUrl || mediaConfig.avatars.defaultFallback,
|
avatarUrl: f.avatarUrl || mediaConfig.avatars.defaultFallback,
|
||||||
href: `/drivers/${f.id}`,
|
href: `/drivers/${f.id}`,
|
||||||
})),
|
})),
|
||||||
friendsCountLabel: String(socialSummary.friendsCount),
|
friendsCountLabel: NumberDisplay.format(socialSummary.friendsCount),
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { SponsorDashboardDTO } from '@/lib/types/generated/SponsorDashboardDTO';
|
import type { SponsorDashboardDTO } from '@/lib/types/generated/SponsorDashboardDTO';
|
||||||
import type { SponsorDashboardViewData } from '@/lib/view-data/SponsorDashboardViewData';
|
import type { SponsorDashboardViewData } from '@/lib/view-data/SponsorDashboardViewData';
|
||||||
|
import { CurrencyDisplay } from '@/lib/display-objects/CurrencyDisplay';
|
||||||
|
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sponsor Dashboard ViewData Builder
|
* Sponsor Dashboard ViewData Builder
|
||||||
@@ -9,26 +11,28 @@ import type { SponsorDashboardViewData } from '@/lib/view-data/SponsorDashboardV
|
|||||||
*/
|
*/
|
||||||
export class SponsorDashboardViewDataBuilder {
|
export class SponsorDashboardViewDataBuilder {
|
||||||
static build(apiDto: SponsorDashboardDTO): SponsorDashboardViewData {
|
static build(apiDto: SponsorDashboardDTO): SponsorDashboardViewData {
|
||||||
|
const totalInvestmentValue = apiDto.investment.activeSponsorships * 1000;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sponsorName: apiDto.sponsorName,
|
sponsorName: apiDto.sponsorName,
|
||||||
totalImpressions: apiDto.metrics.impressions.toString(),
|
totalImpressions: NumberDisplay.format(apiDto.metrics.impressions),
|
||||||
totalInvestment: `$${apiDto.investment.activeSponsorships * 1000}`, // Mock calculation
|
totalInvestment: CurrencyDisplay.format(totalInvestmentValue),
|
||||||
metrics: {
|
metrics: {
|
||||||
impressionsChange: apiDto.metrics.impressions > 1000 ? 15 : -5,
|
impressionsChange: apiDto.metrics.impressions > 1000 ? 15 : -5,
|
||||||
viewersChange: 8,
|
viewersChange: 8,
|
||||||
exposureChange: 12,
|
exposureChange: 12,
|
||||||
},
|
},
|
||||||
categoryData: {
|
categoryData: {
|
||||||
leagues: { count: 2, impressions: 1500 },
|
leagues: { count: 2, countLabel: '2 active', impressions: 1500, impressionsLabel: '1,500' },
|
||||||
teams: { count: 1, impressions: 800 },
|
teams: { count: 1, countLabel: '1 active', impressions: 800, impressionsLabel: '800' },
|
||||||
drivers: { count: 3, impressions: 2200 },
|
drivers: { count: 3, countLabel: '3 active', impressions: 2200, impressionsLabel: '2,200' },
|
||||||
races: { count: 1, impressions: 500 },
|
races: { count: 1, countLabel: '1 active', impressions: 500, impressionsLabel: '500' },
|
||||||
platform: { count: 0, impressions: 0 },
|
platform: { count: 0, countLabel: '0 active', impressions: 0, impressionsLabel: '0' },
|
||||||
},
|
},
|
||||||
sponsorships: apiDto.sponsorships,
|
sponsorships: apiDto.sponsorships,
|
||||||
activeSponsorships: apiDto.investment.activeSponsorships,
|
activeSponsorships: apiDto.investment.activeSponsorships,
|
||||||
formattedTotalInvestment: `$${apiDto.investment.activeSponsorships * 1000}`,
|
formattedTotalInvestment: CurrencyDisplay.format(totalInvestmentValue),
|
||||||
costPerThousandViews: '$50',
|
costPerThousandViews: CurrencyDisplay.format(50),
|
||||||
upcomingRenewals: [], // Mock empty for now
|
upcomingRenewals: [], // Mock empty for now
|
||||||
recentActivity: [], // Mock empty for now
|
recentActivity: [], // Mock empty for now
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { TeamDetailViewData, TeamDetailData, TeamMemberData, SponsorMetric,
|
|||||||
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
||||||
import { MemberDisplay } from '@/lib/display-objects/MemberDisplay';
|
import { MemberDisplay } from '@/lib/display-objects/MemberDisplay';
|
||||||
import { LeagueDisplay } from '@/lib/display-objects/LeagueDisplay';
|
import { LeagueDisplay } from '@/lib/display-objects/LeagueDisplay';
|
||||||
|
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TeamDetailViewDataBuilder - Transforms TeamDetailPageDto into ViewData
|
* TeamDetailViewDataBuilder - Transforms TeamDetailPageDto into ViewData
|
||||||
@@ -47,19 +48,19 @@ export class TeamDetailViewDataBuilder {
|
|||||||
{
|
{
|
||||||
icon: 'users',
|
icon: 'users',
|
||||||
label: 'Members',
|
label: 'Members',
|
||||||
value: memberships.length,
|
value: NumberDisplay.format(memberships.length),
|
||||||
color: 'text-primary-blue',
|
color: 'text-primary-blue',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'zap',
|
icon: 'zap',
|
||||||
label: 'Est. Reach',
|
label: 'Est. Reach',
|
||||||
value: memberships.length * 15,
|
value: NumberDisplay.format(memberships.length * 15),
|
||||||
color: 'text-purple-400',
|
color: 'text-purple-400',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'calendar',
|
icon: 'calendar',
|
||||||
label: 'Races',
|
label: 'Races',
|
||||||
value: leagueCount,
|
value: NumberDisplay.format(leagueCount),
|
||||||
color: 'text-neon-aqua',
|
color: 'text-neon-aqua',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { TeamsPageDto } from '@/lib/page-queries/TeamsPageQuery';
|
import type { TeamsPageDto } from '@/lib/page-queries/TeamsPageQuery';
|
||||||
import type { TeamsViewData, TeamSummaryData } from '@/lib/view-data/TeamsViewData';
|
import type { TeamsViewData, TeamSummaryData } from '@/lib/view-data/TeamsViewData';
|
||||||
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
|
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
|
||||||
|
import { RatingDisplay } from '@/lib/display-objects/RatingDisplay';
|
||||||
|
import { NumberDisplay } from '@/lib/display-objects/NumberDisplay';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TeamsViewDataBuilder - Transforms TeamsPageDto into ViewData for TeamsTemplate
|
* TeamsViewDataBuilder - Transforms TeamsPageDto into ViewData for TeamsTemplate
|
||||||
@@ -14,6 +16,9 @@ export class TeamsViewDataBuilder {
|
|||||||
leagueName: team.leagues[0] || '',
|
leagueName: team.leagues[0] || '',
|
||||||
memberCount: team.memberCount,
|
memberCount: team.memberCount,
|
||||||
logoUrl: team.logoUrl,
|
logoUrl: team.logoUrl,
|
||||||
|
ratingLabel: RatingDisplay.format(team.rating),
|
||||||
|
winsLabel: NumberDisplay.format(team.totalWins || 0),
|
||||||
|
racesLabel: NumberDisplay.format(team.totalRaces || 0),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return { teams };
|
return { teams };
|
||||||
|
|||||||
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, ',');
|
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||||
return parts.join('.');
|
return parts.join('.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a number in compact form (e.g., 1.2k, 1.5M).
|
||||||
|
*/
|
||||||
|
static formatCompact(value: number): string {
|
||||||
|
if (value >= 1000000) {
|
||||||
|
return `${(value / 1000000).toFixed(1)}M`;
|
||||||
|
}
|
||||||
|
if (value >= 1000) {
|
||||||
|
return `${(value / 1000).toFixed(1)}k`;
|
||||||
|
}
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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;
|
const rate = (wins / racesCompleted) * 100;
|
||||||
return rate.toFixed(1);
|
return rate.toFixed(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static format(rate: number | null | undefined): string {
|
||||||
|
if (rate === null || rate === undefined) return '0.0%';
|
||||||
|
return `${rate.toFixed(1)}%`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,8 @@ import { Result } from '@/lib/contracts/Result';
|
|||||||
import type { Service } from '@/lib/contracts/services/Service';
|
import type { Service } from '@/lib/contracts/services/Service';
|
||||||
import type { HomeDataDTO } from '@/lib/types/dtos/HomeDataDTO';
|
import type { HomeDataDTO } from '@/lib/types/dtos/HomeDataDTO';
|
||||||
|
|
||||||
|
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HomeService
|
* HomeService
|
||||||
*
|
*
|
||||||
@@ -54,7 +56,7 @@ export class HomeService implements Service {
|
|||||||
id: r.id,
|
id: r.id,
|
||||||
track: r.track,
|
track: r.track,
|
||||||
car: r.car,
|
car: r.car,
|
||||||
formattedDate: new Date(r.scheduledAt).toLocaleDateString(),
|
formattedDate: DateDisplay.formatShort(r.scheduledAt),
|
||||||
})),
|
})),
|
||||||
topLeagues: leaguesDto.leagues.slice(0, 4).map(l => ({
|
topLeagues: leaguesDto.leagues.slice(0, 4).map(l => ({
|
||||||
id: l.id,
|
id: l.id,
|
||||||
|
|||||||
@@ -6,26 +6,38 @@ export interface DriverProfileViewData {
|
|||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
iracingId: number | null;
|
iracingId: number | null;
|
||||||
joinedAt: string;
|
joinedAt: string;
|
||||||
|
joinedAtLabel: string;
|
||||||
rating: number | null;
|
rating: number | null;
|
||||||
|
ratingLabel: string;
|
||||||
globalRank: number | null;
|
globalRank: number | null;
|
||||||
|
globalRankLabel: string;
|
||||||
consistency: number | null;
|
consistency: number | null;
|
||||||
bio: string | null;
|
bio: string | null;
|
||||||
totalDrivers: number | null;
|
totalDrivers: number | null;
|
||||||
} | null;
|
} | null;
|
||||||
stats: {
|
stats: {
|
||||||
totalRaces: number;
|
totalRaces: number;
|
||||||
|
totalRacesLabel: string;
|
||||||
wins: number;
|
wins: number;
|
||||||
|
winsLabel: string;
|
||||||
podiums: number;
|
podiums: number;
|
||||||
|
podiumsLabel: string;
|
||||||
dnfs: number;
|
dnfs: number;
|
||||||
|
dnfsLabel: string;
|
||||||
avgFinish: number | null;
|
avgFinish: number | null;
|
||||||
|
avgFinishLabel: string;
|
||||||
bestFinish: number | null;
|
bestFinish: number | null;
|
||||||
|
bestFinishLabel: string;
|
||||||
worstFinish: number | null;
|
worstFinish: number | null;
|
||||||
|
worstFinishLabel: string;
|
||||||
finishRate: number | null;
|
finishRate: number | null;
|
||||||
winRate: number | null;
|
winRate: number | null;
|
||||||
podiumRate: number | null;
|
podiumRate: number | null;
|
||||||
percentile: number | null;
|
percentile: number | null;
|
||||||
rating: number | null;
|
rating: number | null;
|
||||||
|
ratingLabel: string;
|
||||||
consistency: number | null;
|
consistency: number | null;
|
||||||
|
consistencyLabel: string;
|
||||||
overallRank: number | null;
|
overallRank: number | null;
|
||||||
} | null;
|
} | null;
|
||||||
finishDistribution: {
|
finishDistribution: {
|
||||||
@@ -42,6 +54,7 @@ export interface DriverProfileViewData {
|
|||||||
teamTag: string | null;
|
teamTag: string | null;
|
||||||
role: string;
|
role: string;
|
||||||
joinedAt: string;
|
joinedAt: string;
|
||||||
|
joinedAtLabel: string;
|
||||||
isCurrent: boolean;
|
isCurrent: boolean;
|
||||||
}[];
|
}[];
|
||||||
socialSummary: {
|
socialSummary: {
|
||||||
@@ -65,7 +78,9 @@ export interface DriverProfileViewData {
|
|||||||
description: string;
|
description: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
rarity: string;
|
rarity: string;
|
||||||
|
rarityLabel: string;
|
||||||
earnedAt: string;
|
earnedAt: string;
|
||||||
|
earnedAtLabel: string;
|
||||||
}[];
|
}[];
|
||||||
racingStyle: string;
|
racingStyle: string;
|
||||||
favoriteTrack: string;
|
favoriteTrack: string;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export interface DriversViewData {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
rating: number;
|
rating: number;
|
||||||
|
ratingLabel: string;
|
||||||
skillLevel: string;
|
skillLevel: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
nationality: string;
|
nationality: string;
|
||||||
@@ -14,6 +15,10 @@ export interface DriversViewData {
|
|||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
}[];
|
}[];
|
||||||
totalRaces: number;
|
totalRaces: number;
|
||||||
|
totalRacesLabel: string;
|
||||||
totalWins: number;
|
totalWins: number;
|
||||||
|
totalWinsLabel: string;
|
||||||
activeCount: number;
|
activeCount: number;
|
||||||
|
activeCountLabel: string;
|
||||||
|
totalDriversLabel: string;
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ export interface RosterMemberData {
|
|||||||
};
|
};
|
||||||
role: string;
|
role: string;
|
||||||
joinedAt: string;
|
joinedAt: string;
|
||||||
|
formattedJoinedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JoinRequestData {
|
export interface JoinRequestData {
|
||||||
@@ -20,6 +21,7 @@ export interface JoinRequestData {
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
requestedAt: string;
|
requestedAt: string;
|
||||||
|
formattedRequestedAt: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ export interface SponsorDashboardViewData {
|
|||||||
exposureChange: number;
|
exposureChange: number;
|
||||||
};
|
};
|
||||||
categoryData: {
|
categoryData: {
|
||||||
leagues: { count: number; impressions: number };
|
leagues: { count: number; countLabel: string; impressions: number; impressionsLabel: string };
|
||||||
teams: { count: number; impressions: number };
|
teams: { count: number; countLabel: string; impressions: number; impressionsLabel: string };
|
||||||
drivers: { count: number; impressions: number };
|
drivers: { count: number; countLabel: string; impressions: number; impressionsLabel: string };
|
||||||
races: { count: number; impressions: number };
|
races: { count: number; countLabel: string; impressions: number; impressionsLabel: string };
|
||||||
platform: { count: number; impressions: number };
|
platform: { count: number; countLabel: string; impressions: number; impressionsLabel: string };
|
||||||
};
|
};
|
||||||
sponsorships: Record<string, unknown>; // From DTO
|
sponsorships: Record<string, unknown>; // From DTO
|
||||||
activeSponsorships: number;
|
activeSponsorships: number;
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
export interface SponsorMetric {
|
export interface SponsorMetric {
|
||||||
icon: string; // Icon name (e.g. 'users', 'zap', 'calendar')
|
icon: string; // Icon name (e.g. 'users', 'zap', 'calendar')
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
trend?: {
|
trend?: {
|
||||||
value: number;
|
value: string;
|
||||||
isPositive: boolean;
|
isPositive: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ export interface TeamSummaryData {
|
|||||||
leagueName: string;
|
leagueName: string;
|
||||||
memberCount: number;
|
memberCount: number;
|
||||||
logoUrl?: string;
|
logoUrl?: string;
|
||||||
|
ratingLabel: string;
|
||||||
|
winsLabel: string;
|
||||||
|
racesLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TeamsViewData extends ViewData {
|
export interface TeamsViewData extends ViewData {
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export interface LeagueSponsorshipsViewData {
|
|||||||
sponsorId: string;
|
sponsorId: string;
|
||||||
sponsorName: string;
|
sponsorName: string;
|
||||||
requestedAt: string;
|
requestedAt: string;
|
||||||
|
formattedRequestedAt: string;
|
||||||
status: 'pending' | 'approved' | 'rejected';
|
status: 'pending' | 'approved' | 'rejected';
|
||||||
|
statusLabel: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,8 @@ export interface LeagueScheduleRaceViewModel {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
scheduledAt: Date;
|
scheduledAt: Date;
|
||||||
|
formattedDate: string;
|
||||||
|
formattedTime: string;
|
||||||
isPast: boolean;
|
isPast: boolean;
|
||||||
isUpcoming: boolean;
|
isUpcoming: boolean;
|
||||||
status: string;
|
status: string;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { ProtestDTO } from '@/lib/types/generated/ProtestDTO';
|
import { ProtestDTO } from '@/lib/types/generated/ProtestDTO';
|
||||||
import { RaceProtestDTO } from '@/lib/types/generated/RaceProtestDTO';
|
import { RaceProtestDTO } from '@/lib/types/generated/RaceProtestDTO';
|
||||||
|
import { DateDisplay } from '../display-objects/DateDisplay';
|
||||||
|
import { StatusDisplay } from '../display-objects/StatusDisplay';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Protest view model
|
* Protest view model
|
||||||
@@ -96,11 +98,11 @@ export class ProtestViewModel {
|
|||||||
|
|
||||||
/** UI-specific: Formatted submitted date */
|
/** UI-specific: Formatted submitted date */
|
||||||
get formattedSubmittedAt(): string {
|
get formattedSubmittedAt(): string {
|
||||||
return new Date(this.submittedAt).toLocaleString();
|
return DateDisplay.formatShort(this.submittedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** UI-specific: Status display */
|
/** UI-specific: Status display */
|
||||||
get statusDisplay(): string {
|
get statusDisplay(): string {
|
||||||
return 'Pending';
|
return StatusDisplay.protestStatus(this.status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { RaceResultDTO } from '@/lib/types/generated/RaceResultDTO';
|
import { RaceResultDTO } from '@/lib/types/generated/RaceResultDTO';
|
||||||
|
import { FinishDisplay } from '../display-objects/FinishDisplay';
|
||||||
|
|
||||||
export class RaceResultViewModel {
|
export class RaceResultViewModel {
|
||||||
driverId!: string;
|
driverId!: string;
|
||||||
@@ -42,7 +43,7 @@ export class RaceResultViewModel {
|
|||||||
|
|
||||||
/** UI-specific: Badge for position */
|
/** UI-specific: Badge for position */
|
||||||
get positionBadge(): string {
|
get positionBadge(): string {
|
||||||
return this.position.toString();
|
return FinishDisplay.format(this.position);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** UI-specific: Color for incidents badge */
|
/** UI-specific: Color for incidents badge */
|
||||||
@@ -66,6 +67,25 @@ export class RaceResultViewModel {
|
|||||||
return this.positionChange;
|
return this.positionChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get formattedPosition(): string {
|
||||||
|
return FinishDisplay.format(this.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
get formattedStartPosition(): string {
|
||||||
|
return FinishDisplay.format(this.startPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
get formattedIncidents(): string {
|
||||||
|
return `${this.incidents}x incidents`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get formattedPositionsGained(): string | undefined {
|
||||||
|
if (this.position < this.startPosition) {
|
||||||
|
return `+${this.startPosition - this.position} positions`;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// Note: The generated DTO doesn't have id or raceId
|
// Note: The generated DTO doesn't have id or raceId
|
||||||
// These will need to be added when the OpenAPI spec is updated
|
// These will need to be added when the OpenAPI spec is updated
|
||||||
id: string = '';
|
id: string = '';
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { CurrencyDisplay } from '../display-objects/CurrencyDisplay';
|
||||||
|
import { DateDisplay } from '../display-objects/DateDisplay';
|
||||||
|
import { NumberDisplay } from '../display-objects/NumberDisplay';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for sponsorship data input
|
* Interface for sponsorship data input
|
||||||
*/
|
*/
|
||||||
@@ -69,11 +73,11 @@ export class SponsorshipViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get formattedImpressions(): string {
|
get formattedImpressions(): string {
|
||||||
return this.impressions.toLocaleString();
|
return NumberDisplay.format(this.impressions);
|
||||||
}
|
}
|
||||||
|
|
||||||
get formattedPrice(): string {
|
get formattedPrice(): string {
|
||||||
return `$${this.price}`;
|
return CurrencyDisplay.format(this.price);
|
||||||
}
|
}
|
||||||
|
|
||||||
get daysRemaining(): number {
|
get daysRemaining(): number {
|
||||||
@@ -109,8 +113,8 @@ export class SponsorshipViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get periodDisplay(): string {
|
get periodDisplay(): string {
|
||||||
const start = this.startDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
|
const start = DateDisplay.formatMonthYear(this.startDate);
|
||||||
const end = this.endDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
|
const end = DateDisplay.formatMonthYear(this.endDate);
|
||||||
return `${start} - ${end}`;
|
return `${start} - ${end}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,12 +75,12 @@ export function DriverProfileTemplate({
|
|||||||
const { currentDriver, stats, teamMemberships, socialSummary, extendedProfile } = viewData;
|
const { currentDriver, stats, teamMemberships, socialSummary, extendedProfile } = viewData;
|
||||||
|
|
||||||
const careerStats = stats ? [
|
const careerStats = stats ? [
|
||||||
{ label: 'Rating', value: stats.rating || 0, color: 'text-primary-blue' },
|
{ label: 'Rating', value: stats.ratingLabel, color: 'text-primary-blue' },
|
||||||
{ label: 'Wins', value: stats.wins, color: 'text-performance-green' },
|
{ label: 'Wins', value: stats.winsLabel, color: 'text-performance-green' },
|
||||||
{ label: 'Podiums', value: stats.podiums, color: 'text-warning-amber' },
|
{ label: 'Podiums', value: stats.podiumsLabel, color: 'text-warning-amber' },
|
||||||
{ label: 'Total Races', value: stats.totalRaces },
|
{ label: 'Total Races', value: stats.totalRacesLabel },
|
||||||
{ label: 'Avg Finish', value: stats.avgFinish?.toFixed(1) || '-', subValue: 'POS' },
|
{ label: 'Avg Finish', value: stats.avgFinishLabel, subValue: 'POS' },
|
||||||
{ label: 'Consistency', value: stats.consistency ? `${stats.consistency}%` : '-' },
|
{ label: 'Consistency', value: stats.consistencyLabel, color: 'text-primary-blue' },
|
||||||
] : [];
|
] : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -115,7 +115,9 @@ export function DriverProfileTemplate({
|
|||||||
avatarUrl={currentDriver.avatarUrl}
|
avatarUrl={currentDriver.avatarUrl}
|
||||||
nationality={currentDriver.country}
|
nationality={currentDriver.country}
|
||||||
rating={stats?.rating || 0}
|
rating={stats?.rating || 0}
|
||||||
globalRank={currentDriver.globalRank ?? undefined}
|
ratingLabel={currentDriver.ratingLabel}
|
||||||
|
safetyRatingLabel="SR 92"
|
||||||
|
globalRankLabel={currentDriver.globalRankLabel}
|
||||||
bio={currentDriver.bio}
|
bio={currentDriver.bio}
|
||||||
friendRequestSent={friendRequestSent}
|
friendRequestSent={friendRequestSent}
|
||||||
onAddFriend={onAddFriend}
|
onAddFriend={onAddFriend}
|
||||||
@@ -132,7 +134,7 @@ export function DriverProfileTemplate({
|
|||||||
memberships={teamMemberships.map((m) => ({
|
memberships={teamMemberships.map((m) => ({
|
||||||
team: { id: m.teamId, name: m.teamName },
|
team: { id: m.teamId, name: m.teamName },
|
||||||
role: m.role,
|
role: m.role,
|
||||||
joinedAt: new Date(m.joinedAt)
|
joinedAtLabel: m.joinedAtLabel
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -172,7 +174,8 @@ export function DriverProfileTemplate({
|
|||||||
<AchievementGrid
|
<AchievementGrid
|
||||||
achievements={extendedProfile.achievements.map((a) => ({
|
achievements={extendedProfile.achievements.map((a) => ({
|
||||||
...a,
|
...a,
|
||||||
earnedAt: new Date(a.earnedAt)
|
rarity: a.rarityLabel,
|
||||||
|
earnedAtLabel: a.earnedAtLabel
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ export function DriversTemplate({
|
|||||||
<Container size="lg" py={8}>
|
<Container size="lg" py={8}>
|
||||||
<Stack gap={10}>
|
<Stack gap={10}>
|
||||||
<DriversDirectoryHeader
|
<DriversDirectoryHeader
|
||||||
totalDrivers={drivers.length}
|
totalDriversLabel={viewData?.totalDriversLabel || '0'}
|
||||||
activeDrivers={activeCount}
|
activeDriversLabel={viewData?.activeCountLabel || '0'}
|
||||||
totalWins={totalWins}
|
totalWinsLabel={viewData?.totalWinsLabel || '0'}
|
||||||
totalRaces={totalRaces}
|
totalRacesLabel={viewData?.totalRacesLabel || '0'}
|
||||||
onViewLeaderboard={onViewLeaderboard}
|
onViewLeaderboard={onViewLeaderboard}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -54,7 +54,8 @@ export function DriversTemplate({
|
|||||||
avatarUrl={driver.avatarUrl}
|
avatarUrl={driver.avatarUrl}
|
||||||
nationality={driver.nationality}
|
nationality={driver.nationality}
|
||||||
rating={driver.rating}
|
rating={driver.rating}
|
||||||
wins={driver.wins}
|
ratingLabel={driver.ratingLabel}
|
||||||
|
winsLabel={String(driver.wins)}
|
||||||
onClick={() => onDriverClick(driver.id)}
|
onClick={() => onDriverClick(driver.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export function LeagueSponsorshipsTemplate({ viewData }: LeagueSponsorshipsTempl
|
|||||||
key={request.id}
|
key={request.id}
|
||||||
request={{
|
request={{
|
||||||
...request,
|
...request,
|
||||||
slotName: slot?.name || 'Unknown slot'
|
slotName: slot?.name || 'Unknown slot',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ export function LeagueWalletTemplate({ viewData, onExport, transactions }: Leagu
|
|||||||
</SharedBox>
|
</SharedBox>
|
||||||
|
|
||||||
<WalletSummaryPanel
|
<WalletSummaryPanel
|
||||||
balance={viewData.balance}
|
formattedBalance={viewData.formattedBalance}
|
||||||
currency="USD"
|
currency="USD"
|
||||||
transactions={transactions}
|
transactions={viewData.transactions}
|
||||||
onDeposit={() => {}} // Not implemented for leagues yet
|
onDeposit={() => {}} // Not implemented for leagues yet
|
||||||
onWithdraw={() => {}} // Not implemented for leagues yet
|
onWithdraw={() => {}} // Not implemented for leagues yet
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -70,10 +70,9 @@ export function ProfileTemplate({
|
|||||||
...viewData.driver,
|
...viewData.driver,
|
||||||
country: viewData.driver.countryCode,
|
country: viewData.driver.countryCode,
|
||||||
iracingId: Number(viewData.driver.iracingId) || 0,
|
iracingId: Number(viewData.driver.iracingId) || 0,
|
||||||
joinedAt: new Date().toISOString(), // Placeholder
|
|
||||||
}}
|
}}
|
||||||
stats={viewData.stats ? { rating: Number(viewData.stats.ratingLabel) || 0 } : null}
|
stats={viewData.stats ? { ratingLabel: viewData.stats.ratingLabel } : null}
|
||||||
globalRank={Number(viewData.stats?.globalRankLabel) || 0}
|
globalRankLabel={viewData.stats?.globalRankLabel || '—'}
|
||||||
onAddFriend={onFriendRequestSend}
|
onAddFriend={onFriendRequestSend}
|
||||||
friendRequestSent={friendRequestSent}
|
friendRequestSent={friendRequestSent}
|
||||||
isOwnProfile={true}
|
isOwnProfile={true}
|
||||||
@@ -99,7 +98,7 @@ export function ProfileTemplate({
|
|||||||
memberships={viewData.teamMemberships.map(m => ({
|
memberships={viewData.teamMemberships.map(m => ({
|
||||||
team: { id: m.teamId, name: m.teamName },
|
team: { id: m.teamId, name: m.teamName },
|
||||||
role: m.roleLabel,
|
role: m.roleLabel,
|
||||||
joinedAt: new Date() // Placeholder
|
joinedAtLabel: m.joinedAtLabel
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -117,7 +116,7 @@ export function ProfileTemplate({
|
|||||||
achievements={viewData.extendedProfile.achievements.map(a => ({
|
achievements={viewData.extendedProfile.achievements.map(a => ({
|
||||||
...a,
|
...a,
|
||||||
rarity: a.rarityLabel,
|
rarity: a.rarityLabel,
|
||||||
earnedAt: new Date() // Placeholder
|
earnedAtLabel: a.earnedAtLabel
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export function RosterAdminTemplate({
|
|||||||
<Stack direction={{ base: 'col', md: 'row' }} align="center" justify="between" gap={4}>
|
<Stack direction={{ base: 'col', md: 'row' }} align="center" justify="between" gap={4}>
|
||||||
<Stack gap={1}>
|
<Stack gap={1}>
|
||||||
<Text weight="bold" color="text-white">{req.driver.name}</Text>
|
<Text weight="bold" color="text-white">{req.driver.name}</Text>
|
||||||
<Text size="xs" color="text-gray-500">{new Date(req.requestedAt).toLocaleString()}</Text>
|
<Text size="xs" color="text-gray-500">{req.formattedRequestedAt}</Text>
|
||||||
{req.message && (
|
{req.message && (
|
||||||
<Text size="sm" color="text-gray-400" mt={1}>"{req.message}"</Text>
|
<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>
|
<Text weight="bold" color="text-white">{member.driver.name}</Text>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Text size="sm" color="text-gray-400">{new Date(member.joinedAt).toLocaleDateString()}</Text>
|
<Text size="sm" color="text-gray-400">{member.formattedJoinedAt}</Text>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ export interface SponsorCampaignsViewData {
|
|||||||
leagueName: string;
|
leagueName: string;
|
||||||
seasonName: string;
|
seasonName: string;
|
||||||
tier: string;
|
tier: string;
|
||||||
pricing: { amount: number; currency: string };
|
formattedInvestment: string;
|
||||||
metrics: { impressions: number };
|
formattedImpressions: string;
|
||||||
seasonStartDate?: Date;
|
formattedStartDate?: string;
|
||||||
seasonEndDate?: Date;
|
formattedEndDate?: string;
|
||||||
}>;
|
}>;
|
||||||
stats: {
|
stats: {
|
||||||
total: number;
|
total: number;
|
||||||
@@ -38,8 +38,8 @@ export interface SponsorCampaignsViewData {
|
|||||||
pending: number;
|
pending: number;
|
||||||
approved: number;
|
approved: number;
|
||||||
rejected: number;
|
rejected: number;
|
||||||
totalInvestment: number;
|
formattedTotalInvestment: string;
|
||||||
totalImpressions: number;
|
formattedTotalImpressions: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,13 +80,13 @@ export function SponsorCampaignsTemplate({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Total Investment',
|
label: 'Total Investment',
|
||||||
value: `$${viewData.stats.totalInvestment.toLocaleString()}`,
|
value: viewData.stats.formattedTotalInvestment,
|
||||||
icon: BarChart3,
|
icon: BarChart3,
|
||||||
variant: 'info',
|
variant: 'info',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Total Impressions',
|
label: 'Total Impressions',
|
||||||
value: `${(viewData.stats.totalImpressions / 1000).toFixed(0)}k`,
|
value: viewData.stats.formattedTotalImpressions,
|
||||||
icon: Eye,
|
icon: Eye,
|
||||||
variant: 'default',
|
variant: 'default',
|
||||||
},
|
},
|
||||||
@@ -153,10 +153,10 @@ export function SponsorCampaignsTemplate({
|
|||||||
title={s.leagueName}
|
title={s.leagueName}
|
||||||
subtitle={s.seasonName}
|
subtitle={s.seasonName}
|
||||||
tier={s.tier}
|
tier={s.tier}
|
||||||
investment={`$${s.pricing.amount.toLocaleString()}`}
|
investment={s.formattedInvestment}
|
||||||
impressions={s.metrics.impressions.toLocaleString()}
|
impressions={s.formattedImpressions}
|
||||||
startDate={s.seasonStartDate ? new Date(s.seasonStartDate).toLocaleDateString() : undefined}
|
startDate={s.formattedStartDate}
|
||||||
endDate={s.seasonEndDate ? new Date(s.seasonEndDate).toLocaleDateString() : undefined}
|
endDate={s.formattedEndDate}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -148,40 +148,40 @@ export function SponsorDashboardTemplate({ viewData }: SponsorDashboardTemplateP
|
|||||||
<SponsorshipCategoryCard
|
<SponsorshipCategoryCard
|
||||||
icon={Trophy}
|
icon={Trophy}
|
||||||
title="Leagues"
|
title="Leagues"
|
||||||
count={categoryData.leagues.count}
|
countLabel={categoryData.leagues.countLabel}
|
||||||
impressions={categoryData.leagues.impressions}
|
impressionsLabel={categoryData.leagues.impressionsLabel}
|
||||||
color="#3b82f6"
|
color="#3b82f6"
|
||||||
href="/sponsor/campaigns?type=leagues"
|
href="/sponsor/campaigns?type=leagues"
|
||||||
/>
|
/>
|
||||||
<SponsorshipCategoryCard
|
<SponsorshipCategoryCard
|
||||||
icon={Users}
|
icon={Users}
|
||||||
title="Teams"
|
title="Teams"
|
||||||
count={categoryData.teams.count}
|
countLabel={categoryData.teams.countLabel}
|
||||||
impressions={categoryData.teams.impressions}
|
impressionsLabel={categoryData.teams.impressionsLabel}
|
||||||
color="#a855f7"
|
color="#a855f7"
|
||||||
href="/sponsor/campaigns?type=teams"
|
href="/sponsor/campaigns?type=teams"
|
||||||
/>
|
/>
|
||||||
<SponsorshipCategoryCard
|
<SponsorshipCategoryCard
|
||||||
icon={Car}
|
icon={Car}
|
||||||
title="Drivers"
|
title="Drivers"
|
||||||
count={categoryData.drivers.count}
|
countLabel={categoryData.drivers.countLabel}
|
||||||
impressions={categoryData.drivers.impressions}
|
impressionsLabel={categoryData.drivers.impressionsLabel}
|
||||||
color="#10b981"
|
color="#10b981"
|
||||||
href="/sponsor/campaigns?type=drivers"
|
href="/sponsor/campaigns?type=drivers"
|
||||||
/>
|
/>
|
||||||
<SponsorshipCategoryCard
|
<SponsorshipCategoryCard
|
||||||
icon={Flag}
|
icon={Flag}
|
||||||
title="Races"
|
title="Races"
|
||||||
count={categoryData.races.count}
|
countLabel={categoryData.races.countLabel}
|
||||||
impressions={categoryData.races.impressions}
|
impressionsLabel={categoryData.races.impressionsLabel}
|
||||||
color="#f59e0b"
|
color="#f59e0b"
|
||||||
href="/sponsor/campaigns?type=races"
|
href="/sponsor/campaigns?type=races"
|
||||||
/>
|
/>
|
||||||
<SponsorshipCategoryCard
|
<SponsorshipCategoryCard
|
||||||
icon={Megaphone}
|
icon={Megaphone}
|
||||||
title="Platform Ads"
|
title="Platform Ads"
|
||||||
count={categoryData.platform.count}
|
countLabel={categoryData.platform.countLabel}
|
||||||
impressions={categoryData.platform.impressions}
|
impressionsLabel={categoryData.platform.impressionsLabel}
|
||||||
color="#ef4444"
|
color="#ef4444"
|
||||||
href="/sponsor/campaigns?type=platform"
|
href="/sponsor/campaigns?type=platform"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -45,11 +45,16 @@ export interface SponsorLeagueDetailViewData extends ViewData {
|
|||||||
description: string;
|
description: string;
|
||||||
tier: 'premium' | 'standard' | 'starter';
|
tier: 'premium' | 'standard' | 'starter';
|
||||||
rating: number;
|
rating: number;
|
||||||
|
formattedRating: string;
|
||||||
drivers: number;
|
drivers: number;
|
||||||
|
formattedDrivers: string;
|
||||||
races: number;
|
races: number;
|
||||||
|
formattedRaces: string;
|
||||||
completedRaces: number;
|
completedRaces: number;
|
||||||
racesLeft: number;
|
racesLeft: number;
|
||||||
|
formattedRacesLeft: string;
|
||||||
engagement: number;
|
engagement: number;
|
||||||
|
formattedEngagement: string;
|
||||||
totalImpressions: number;
|
totalImpressions: number;
|
||||||
formattedTotalImpressions: string;
|
formattedTotalImpressions: string;
|
||||||
projectedTotal: number;
|
projectedTotal: number;
|
||||||
@@ -61,17 +66,20 @@ export interface SponsorLeagueDetailViewData extends ViewData {
|
|||||||
nextRace?: {
|
nextRace?: {
|
||||||
name: string;
|
name: string;
|
||||||
date: string;
|
date: string;
|
||||||
|
formattedDate: string;
|
||||||
};
|
};
|
||||||
sponsorSlots: {
|
sponsorSlots: {
|
||||||
main: {
|
main: {
|
||||||
available: boolean;
|
available: boolean;
|
||||||
price: number;
|
price: number;
|
||||||
|
priceLabel: string;
|
||||||
benefits: string[];
|
benefits: string[];
|
||||||
};
|
};
|
||||||
secondary: {
|
secondary: {
|
||||||
available: number;
|
available: number;
|
||||||
total: number;
|
total: number;
|
||||||
price: number;
|
price: number;
|
||||||
|
priceLabel: string;
|
||||||
benefits: string[];
|
benefits: string[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -84,10 +92,12 @@ export interface SponsorLeagueDetailViewData extends ViewData {
|
|||||||
drivers: Array<{
|
drivers: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
position: number;
|
position: number;
|
||||||
|
positionLabel: string;
|
||||||
name: string;
|
name: string;
|
||||||
team: string;
|
team: string;
|
||||||
country: string;
|
country: string;
|
||||||
races: number;
|
races: number;
|
||||||
|
formattedRaces: string;
|
||||||
impressions: number;
|
impressions: number;
|
||||||
formattedImpressions: string;
|
formattedImpressions: string;
|
||||||
}>;
|
}>;
|
||||||
@@ -98,6 +108,7 @@ export interface SponsorLeagueDetailViewData extends ViewData {
|
|||||||
formattedDate: string;
|
formattedDate: string;
|
||||||
status: 'completed' | 'upcoming';
|
status: 'completed' | 'upcoming';
|
||||||
views: number;
|
views: number;
|
||||||
|
formattedViews: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,13 +150,13 @@ export function SponsorLeagueDetailTemplate({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Engagement',
|
label: 'Engagement',
|
||||||
value: `${league.engagement}%`,
|
value: league.formattedEngagement,
|
||||||
icon: BarChart3,
|
icon: BarChart3,
|
||||||
variant: 'warning',
|
variant: 'warning',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Races Left',
|
label: 'Races Left',
|
||||||
value: league.racesLeft,
|
value: league.formattedRacesLeft,
|
||||||
icon: Calendar,
|
icon: Calendar,
|
||||||
variant: 'default',
|
variant: 'default',
|
||||||
},
|
},
|
||||||
@@ -156,6 +167,7 @@ export function SponsorLeagueDetailTemplate({
|
|||||||
id: 'main',
|
id: 'main',
|
||||||
name: 'Main Sponsor',
|
name: 'Main Sponsor',
|
||||||
price: league.sponsorSlots.main.price,
|
price: league.sponsorSlots.main.price,
|
||||||
|
priceLabel: league.sponsorSlots.main.priceLabel,
|
||||||
period: 'Season',
|
period: 'Season',
|
||||||
description: 'Exclusive primary branding across all league assets.',
|
description: 'Exclusive primary branding across all league assets.',
|
||||||
features: league.sponsorSlots.main.benefits,
|
features: league.sponsorSlots.main.benefits,
|
||||||
@@ -166,6 +178,7 @@ export function SponsorLeagueDetailTemplate({
|
|||||||
id: 'secondary',
|
id: 'secondary',
|
||||||
name: 'Secondary Sponsor',
|
name: 'Secondary Sponsor',
|
||||||
price: league.sponsorSlots.secondary.price,
|
price: league.sponsorSlots.secondary.price,
|
||||||
|
priceLabel: league.sponsorSlots.secondary.priceLabel,
|
||||||
period: 'Season',
|
period: 'Season',
|
||||||
description: 'Supporting branding on cars and broadcast overlays.',
|
description: 'Supporting branding on cars and broadcast overlays.',
|
||||||
features: league.sponsorSlots.secondary.benefits,
|
features: league.sponsorSlots.secondary.benefits,
|
||||||
@@ -238,8 +251,8 @@ export function SponsorLeagueDetailTemplate({
|
|||||||
<InfoRow label="Platform" value={league.game} />
|
<InfoRow label="Platform" value={league.game} />
|
||||||
<InfoRow label="Season" value={league.season} />
|
<InfoRow label="Season" value={league.season} />
|
||||||
<InfoRow label="Duration" value="Oct 2025 - Feb 2026" />
|
<InfoRow label="Duration" value="Oct 2025 - Feb 2026" />
|
||||||
<InfoRow label="Drivers" value={league.drivers} />
|
<InfoRow label="Drivers" value={league.formattedDrivers} />
|
||||||
<InfoRow label="Races" value={league.races} last />
|
<InfoRow label="Races" value={league.formattedRaces} last />
|
||||||
</SharedStack>
|
</SharedStack>
|
||||||
</SharedCard>
|
</SharedCard>
|
||||||
|
|
||||||
@@ -253,8 +266,8 @@ export function SponsorLeagueDetailTemplate({
|
|||||||
<InfoRow label="Total Season Views" value={league.formattedTotalImpressions} />
|
<InfoRow label="Total Season Views" value={league.formattedTotalImpressions} />
|
||||||
<InfoRow label="Projected Total" value={league.formattedProjectedTotal} />
|
<InfoRow label="Projected Total" value={league.formattedProjectedTotal} />
|
||||||
<InfoRow label="Main Sponsor CPM" value={league.formattedMainSponsorCpm} color="text-performance-green" />
|
<InfoRow label="Main Sponsor CPM" value={league.formattedMainSponsorCpm} color="text-performance-green" />
|
||||||
<InfoRow label="Engagement Rate" value={`${league.engagement}%`} />
|
<InfoRow label="Engagement Rate" value={league.formattedEngagement} />
|
||||||
<InfoRow label="League Rating" value={`${league.rating}/5.0`} last />
|
<InfoRow label="League Rating" value={league.formattedRating} last />
|
||||||
</SharedStack>
|
</SharedStack>
|
||||||
</SharedCard>
|
</SharedCard>
|
||||||
|
|
||||||
@@ -274,7 +287,7 @@ export function SponsorLeagueDetailTemplate({
|
|||||||
</Surface>
|
</Surface>
|
||||||
<SharedBox>
|
<SharedBox>
|
||||||
<SharedText size="lg" weight="semibold" color="text-white" block>{league.nextRace.name}</SharedText>
|
<SharedText size="lg" weight="semibold" color="text-white" block>{league.nextRace.name}</SharedText>
|
||||||
<SharedText size="sm" color="text-gray-400" block mt={1}>{league.nextRace.date}</SharedText>
|
<SharedText size="sm" color="text-gray-400" block mt={1}>{league.nextRace.formattedDate}</SharedText>
|
||||||
</SharedBox>
|
</SharedBox>
|
||||||
</SharedStack>
|
</SharedStack>
|
||||||
<SharedButton variant="secondary">
|
<SharedButton variant="secondary">
|
||||||
@@ -300,7 +313,7 @@ export function SponsorLeagueDetailTemplate({
|
|||||||
<SharedStack direction="row" align="center" justify="between">
|
<SharedStack direction="row" align="center" justify="between">
|
||||||
<SharedStack direction="row" align="center" gap={4}>
|
<SharedStack direction="row" align="center" gap={4}>
|
||||||
<Surface variant="muted" rounded="full" padding={1} style={{ width: '2.5rem', height: '2.5rem', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#262626' }}>
|
<Surface variant="muted" rounded="full" padding={1} style={{ width: '2.5rem', height: '2.5rem', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#262626' }}>
|
||||||
<SharedText weight="bold" color="text-white">{driver.position}</SharedText>
|
<SharedText weight="bold" color="text-white">{driver.positionLabel}</SharedText>
|
||||||
</Surface>
|
</Surface>
|
||||||
<SharedBox>
|
<SharedBox>
|
||||||
<SharedText weight="medium" color="text-white" block>{driver.name}</SharedText>
|
<SharedText weight="medium" color="text-white" block>{driver.name}</SharedText>
|
||||||
@@ -309,7 +322,7 @@ export function SponsorLeagueDetailTemplate({
|
|||||||
</SharedStack>
|
</SharedStack>
|
||||||
<SharedStack direction="row" align="center" gap={8}>
|
<SharedStack direction="row" align="center" gap={8}>
|
||||||
<SharedBox textAlign="right">
|
<SharedBox textAlign="right">
|
||||||
<SharedText weight="medium" color="text-white" block>{driver.races}</SharedText>
|
<SharedText weight="medium" color="text-white" block>{driver.formattedRaces}</SharedText>
|
||||||
<SharedText size="xs" color="text-gray-500">races</SharedText>
|
<SharedText size="xs" color="text-gray-500">races</SharedText>
|
||||||
</SharedBox>
|
</SharedBox>
|
||||||
<SharedBox textAlign="right">
|
<SharedBox textAlign="right">
|
||||||
@@ -344,7 +357,7 @@ export function SponsorLeagueDetailTemplate({
|
|||||||
<SharedBox>
|
<SharedBox>
|
||||||
{race.status === 'completed' ? (
|
{race.status === 'completed' ? (
|
||||||
<SharedBox textAlign="right">
|
<SharedBox textAlign="right">
|
||||||
<SharedText weight="semibold" color="text-white" block>{race.views.toLocaleString()}</SharedText>
|
<SharedText weight="semibold" color="text-white" block>{race.formattedViews}</SharedText>
|
||||||
<SharedText size="xs" color="text-gray-500">views</SharedText>
|
<SharedText size="xs" color="text-gray-500">views</SharedText>
|
||||||
</SharedBox>
|
</SharedBox>
|
||||||
) : (
|
) : (
|
||||||
@@ -384,7 +397,7 @@ export function SponsorLeagueDetailTemplate({
|
|||||||
|
|
||||||
<SharedStack gap={3} mb={6}>
|
<SharedStack gap={3} mb={6}>
|
||||||
<InfoRow label="Selected Tier" value={`${selectedTier.charAt(0).toUpperCase() + selectedTier.slice(1)} Sponsor`} />
|
<InfoRow label="Selected Tier" value={`${selectedTier.charAt(0).toUpperCase() + selectedTier.slice(1)} Sponsor`} />
|
||||||
<InfoRow label="Season Price" value={`$${selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price}`} />
|
<InfoRow label="Season Price" value={selectedTier === 'main' ? league.sponsorSlots.main.priceLabel : league.sponsorSlots.secondary.priceLabel} />
|
||||||
<InfoRow label={`Platform Fee (${siteConfig.fees.platformFeePercent}%)`} value={`$${((selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price) * siteConfig.fees.platformFeePercent / 100).toFixed(2)}`} />
|
<InfoRow label={`Platform Fee (${siteConfig.fees.platformFeePercent}%)`} value={`$${((selectedTier === 'main' ? league.sponsorSlots.main.price : league.sponsorSlots.secondary.price) * siteConfig.fees.platformFeePercent / 100).toFixed(2)}`} />
|
||||||
<SharedBox pt={4} borderTop borderColor="border-neutral-800">
|
<SharedBox pt={4} borderTop borderColor="border-neutral-800">
|
||||||
<SharedStack direction="row" align="center" justify="between">
|
<SharedStack direction="row" align="center" justify="between">
|
||||||
|
|||||||
@@ -96,9 +96,9 @@ export function TeamDetailTemplate({
|
|||||||
entityName={team.name}
|
entityName={team.name}
|
||||||
tier="standard"
|
tier="standard"
|
||||||
metrics={viewData.teamMetrics}
|
metrics={viewData.teamMetrics}
|
||||||
slots={SlotTemplates.team(true, true, 500, 250)}
|
slots={SlotTemplates.team(true, true, '$500', '$250')}
|
||||||
trustScore={90}
|
trustScoreLabel="90/100"
|
||||||
monthlyActivity={85}
|
monthlyActivityLabel="85%"
|
||||||
onNavigate={(href) => window.location.href = href}
|
onNavigate={(href) => window.location.href = href}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ export function TeamsTemplate({ viewData, onTeamClick, onViewFullLeaderboard, on
|
|||||||
name={team.teamName}
|
name={team.teamName}
|
||||||
memberCount={team.memberCount}
|
memberCount={team.memberCount}
|
||||||
logo={team.logoUrl}
|
logo={team.logoUrl}
|
||||||
|
ratingLabel={team.ratingLabel}
|
||||||
|
winsLabel={team.winsLabel}
|
||||||
|
racesLabel={team.racesLabel}
|
||||||
onClick={() => onTeamClick?.(team.teamId)}
|
onClick={() => onTeamClick?.(team.teamId)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,106 +1,2 @@
|
|||||||
adapters/bootstrap/SeedDemoUsers.ts(6,1): error TS6133: 'UserRepository' is declared but its value is never read.
|
apps/website/components/teams/TeamAdmin.tsx(177,17): error TS2322: Type '{ key: string; driverId: string; requestedAt: string; onApprove: () => void; onReject: () => void; isApproving: boolean; isRejecting: boolean; }' is not assignable to type 'IntrinsicAttributes & JoinRequestItemProps'.
|
||||||
apps/api/src/domain/analytics/AnalyticsProviders.ts(3,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
|
Property 'requestedAt' does not exist on type 'IntrinsicAttributes & JoinRequestItemProps'.
|
||||||
apps/api/src/domain/auth/AuthProviders.ts(16,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
|
|
||||||
apps/api/src/domain/auth/AuthService.ts(3,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
|
|
||||||
apps/api/src/domain/bootstrap/BootstrapModule.ts(1,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
|
|
||||||
apps/api/src/domain/bootstrap/BootstrapProviders.ts(8,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
|
|
||||||
apps/api/src/domain/dashboard/DashboardProviders.ts(11,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
|
|
||||||
apps/api/src/domain/dashboard/DashboardService.ts(7,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
|
|
||||||
apps/api/src/domain/driver/DriverProviders.ts(167,14): error TS2693: 'RankingUseCase' only refers to a type, but is being used as a value here.
|
|
||||||
apps/api/src/domain/driver/DriverProviders.ts(176,14): error TS2693: 'DriverStatsUseCase' only refers to a type, but is being used as a value here.
|
|
||||||
apps/api/src/domain/driver/DriverService.ts(33,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
|
|
||||||
apps/api/src/domain/league/LeagueProviders.ts(16,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
|
|
||||||
apps/api/src/domain/league/LeagueService.ts(61,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
|
|
||||||
apps/api/src/domain/media/MediaController.ts(3,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
|
|
||||||
apps/api/src/domain/media/MediaProviders.ts(7,10): error TS2724: '"@core/media/domain/repositories/AvatarGenerationRepository"' has no exported member named 'IAvatarGenerationRepository'. Did you mean 'AvatarGenerationRepository'?
|
|
||||||
apps/api/src/domain/media/MediaProviders.ts(8,10): error TS2724: '"@core/media/domain/repositories/AvatarRepository"' has no exported member named 'IAvatarRepository'. Did you mean 'AvatarRepository'?
|
|
||||||
apps/api/src/domain/media/MediaProviders.ts(9,10): error TS2724: '"@core/media/domain/repositories/MediaRepository"' has no exported member named 'IMediaRepository'. Did you mean 'MediaRepository'?
|
|
||||||
apps/api/src/domain/media/MediaProviders.ts(11,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
|
|
||||||
apps/api/src/domain/media/MediaService.ts(39,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
|
|
||||||
apps/api/src/domain/notifications/NotificationsProviders.ts(5,10): error TS2724: '"@core/notifications/domain/repositories/NotificationRepository"' has no exported member named 'INotificationRepository'. Did you mean 'NotificationRepository'?
|
|
||||||
apps/api/src/domain/notifications/NotificationsProviders.ts(6,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
|
|
||||||
apps/api/src/domain/payments/PaymentsProviders.ts(4,15): error TS2724: '"@core/payments/domain/repositories/MembershipFeeRepository"' has no exported member named 'IMemberPaymentRepository'. Did you mean 'MemberPaymentRepository'?
|
|
||||||
apps/api/src/domain/payments/PaymentsProviders.ts(4,41): error TS2724: '"@core/payments/domain/repositories/MembershipFeeRepository"' has no exported member named 'IMembershipFeeRepository'. Did you mean 'MembershipFeeRepository'?
|
|
||||||
apps/api/src/domain/payments/PaymentsProviders.ts(5,15): error TS2724: '"@core/payments/domain/repositories/PaymentRepository"' has no exported member named 'IPaymentRepository'. Did you mean 'PaymentRepository'?
|
|
||||||
apps/api/src/domain/payments/PaymentsProviders.ts(6,15): error TS2724: '"@core/payments/domain/repositories/PrizeRepository"' has no exported member named 'IPrizeRepository'. Did you mean 'PrizeRepository'?
|
|
||||||
apps/api/src/domain/payments/PaymentsProviders.ts(7,15): error TS2724: '"@core/payments/domain/repositories/WalletRepository"' has no exported member named 'ITransactionRepository'. Did you mean 'TransactionRepository'?
|
|
||||||
apps/api/src/domain/payments/PaymentsProviders.ts(7,39): error TS2724: '"@core/payments/domain/repositories/WalletRepository"' has no exported member named 'IWalletRepository'. Did you mean 'WalletRepository'?
|
|
||||||
apps/api/src/domain/payments/PaymentsService.ts(1,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
|
|
||||||
apps/api/src/domain/protests/ProtestsProviders.ts(4,15): error TS2724: '"@core/racing/domain/repositories/LeagueMembershipRepository"' has no exported member named 'ILeagueMembershipRepository'. Did you mean 'LeagueMembershipRepository'?
|
|
||||||
apps/api/src/domain/protests/ProtestsProviders.ts(5,15): error TS2724: '"@core/racing/domain/repositories/ProtestRepository"' has no exported member named 'IProtestRepository'. Did you mean 'ProtestRepository'?
|
|
||||||
apps/api/src/domain/protests/ProtestsProviders.ts(6,15): error TS2724: '"@core/racing/domain/repositories/RaceRepository"' has no exported member named 'IRaceRepository'. Did you mean 'RaceRepository'?
|
|
||||||
apps/api/src/domain/protests/ProtestsProviders.ts(7,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
|
|
||||||
apps/api/src/domain/protests/ProtestsService.test.ts(6,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
|
|
||||||
apps/api/src/domain/protests/ProtestsService.ts(1,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
|
|
||||||
apps/api/src/domain/race/presenters/RaceDetailPresenter.ts(2,15): error TS2724: '"@core/racing/application/ports/ImageServicePort"' has no exported member named 'IImageServicePort'. Did you mean 'ImageServicePort'?
|
|
||||||
apps/api/src/domain/race/presenters/RacePenaltiesPresenter.ts(15,74): error TS2352: Conversion of type '{ id: PenaltyId; driverId: string; type: string; value: number; reason: string; issuedBy: string; issuedAt: string; notes: string | undefined; }' to type 'RacePenaltyDTO' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
|
|
||||||
Types of property 'id' are incompatible.
|
|
||||||
Type 'PenaltyId' is not comparable to type 'string'.
|
|
||||||
apps/api/src/domain/race/presenters/RaceResultsDetailPresenter.ts(1,15): error TS2724: '"@core/racing/application/ports/ImageServicePort"' has no exported member named 'IImageServicePort'. Did you mean 'ImageServicePort'?
|
|
||||||
apps/api/src/domain/race/RaceProviders.ts(5,15): error TS2724: '"@core/racing/domain/repositories/DriverRepository"' has no exported member named 'IDriverRepository'. Did you mean 'DriverRepository'?
|
|
||||||
apps/api/src/domain/race/RaceProviders.ts(6,15): error TS2724: '"@core/racing/domain/repositories/LeagueMembershipRepository"' has no exported member named 'ILeagueMembershipRepository'. Did you mean 'LeagueMembershipRepository'?
|
|
||||||
apps/api/src/domain/race/RaceProviders.ts(7,15): error TS2724: '"@core/racing/domain/repositories/LeagueRepository"' has no exported member named 'ILeagueRepository'. Did you mean 'LeagueRepository'?
|
|
||||||
apps/api/src/domain/race/RaceProviders.ts(8,15): error TS2724: '"@core/racing/domain/repositories/PenaltyRepository"' has no exported member named 'IPenaltyRepository'. Did you mean 'PenaltyRepository'?
|
|
||||||
apps/api/src/domain/race/RaceProviders.ts(9,15): error TS2724: '"@core/racing/domain/repositories/ProtestRepository"' has no exported member named 'IProtestRepository'. Did you mean 'ProtestRepository'?
|
|
||||||
apps/api/src/domain/race/RaceProviders.ts(10,15): error TS2724: '"@core/racing/domain/repositories/RaceRegistrationRepository"' has no exported member named 'IRaceRegistrationRepository'. Did you mean 'RaceRegistrationRepository'?
|
|
||||||
apps/api/src/domain/race/RaceProviders.ts(11,15): error TS2724: '"@core/racing/domain/repositories/RaceRepository"' has no exported member named 'IRaceRepository'. Did you mean 'RaceRepository'?
|
|
||||||
apps/api/src/domain/race/RaceProviders.ts(12,15): error TS2724: '"@core/racing/domain/repositories/ResultRepository"' has no exported member named 'IResultRepository'. Did you mean 'ResultRepository'?
|
|
||||||
apps/api/src/domain/race/RaceProviders.ts(13,15): error TS2724: '"@core/racing/domain/repositories/StandingRepository"' has no exported member named 'IStandingRepository'. Did you mean 'StandingRepository'?
|
|
||||||
apps/api/src/domain/race/RaceProviders.ts(14,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
|
|
||||||
apps/api/src/domain/race/RaceService.ts(11,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
|
|
||||||
apps/api/src/domain/sponsor/SponsorProviders.ts(6,15): error TS2724: '"@core/payments/domain/repositories/PaymentRepository"' has no exported member named 'IPaymentRepository'. Did you mean 'PaymentRepository'?
|
|
||||||
apps/api/src/domain/sponsor/SponsorProviders.ts(7,15): error TS2724: '"@core/payments/domain/repositories/WalletRepository"' has no exported member named 'IWalletRepository'. Did you mean 'WalletRepository'?
|
|
||||||
apps/api/src/domain/sponsor/SponsorProviders.ts(8,10): error TS2724: '"@core/racing/domain/repositories/LeagueMembershipRepository"' has no exported member named 'ILeagueMembershipRepository'. Did you mean 'LeagueMembershipRepository'?
|
|
||||||
apps/api/src/domain/sponsor/SponsorProviders.ts(9,10): error TS2724: '"@core/racing/domain/repositories/LeagueRepository"' has no exported member named 'ILeagueRepository'. Did you mean 'LeagueRepository'?
|
|
||||||
apps/api/src/domain/sponsor/SponsorProviders.ts(10,10): error TS2724: '"@core/racing/domain/repositories/LeagueWalletRepository"' has no exported member named 'ILeagueWalletRepository'. Did you mean 'LeagueWalletRepository'?
|
|
||||||
apps/api/src/domain/sponsor/SponsorProviders.ts(11,10): error TS2724: '"@core/racing/domain/repositories/RaceRepository"' has no exported member named 'IRaceRepository'. Did you mean 'RaceRepository'?
|
|
||||||
apps/api/src/domain/sponsor/SponsorProviders.ts(12,10): error TS2724: '"@core/racing/domain/repositories/SeasonRepository"' has no exported member named 'ISeasonRepository'. Did you mean 'SeasonRepository'?
|
|
||||||
apps/api/src/domain/sponsor/SponsorProviders.ts(13,10): error TS2724: '"@core/racing/domain/repositories/SeasonSponsorshipRepository"' has no exported member named 'ISeasonSponsorshipRepository'. Did you mean 'SeasonSponsorshipRepository'?
|
|
||||||
apps/api/src/domain/sponsor/SponsorProviders.ts(14,10): error TS2724: '"@core/racing/domain/repositories/SponsorRepository"' has no exported member named 'ISponsorRepository'. Did you mean 'SponsorRepository'?
|
|
||||||
apps/api/src/domain/sponsor/SponsorProviders.ts(15,10): error TS2724: '"@core/racing/domain/repositories/SponsorshipPricingRepository"' has no exported member named 'ISponsorshipPricingRepository'. Did you mean 'SponsorshipPricingRepository'?
|
|
||||||
apps/api/src/domain/sponsor/SponsorProviders.ts(16,10): error TS2724: '"@core/racing/domain/repositories/SponsorshipRequestRepository"' has no exported member named 'ISponsorshipRequestRepository'. Did you mean 'SponsorshipRequestRepository'?
|
|
||||||
apps/api/src/domain/sponsor/SponsorProviders.ts(17,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
|
|
||||||
apps/api/src/domain/sponsor/SponsorService.test.ts(13,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
|
|
||||||
apps/api/src/domain/sponsor/SponsorService.ts(38,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
|
|
||||||
apps/api/src/domain/team/TeamProviders.ts(33,15): error TS2724: '"@core/racing/domain/repositories/DriverRepository"' has no exported member named 'IDriverRepository'. Did you mean 'DriverRepository'?
|
|
||||||
apps/api/src/domain/team/TeamProviders.ts(34,15): error TS2724: '"@core/racing/domain/repositories/TeamMembershipRepository"' has no exported member named 'ITeamMembershipRepository'. Did you mean 'TeamMembershipRepository'?
|
|
||||||
apps/api/src/domain/team/TeamProviders.ts(35,15): error TS2724: '"@core/racing/domain/repositories/TeamRepository"' has no exported member named 'ITeamRepository'. Did you mean 'TeamRepository'?
|
|
||||||
apps/api/src/domain/team/TeamProviders.ts(36,15): error TS2724: '"@core/racing/domain/repositories/TeamStatsRepository"' has no exported member named 'ITeamStatsRepository'. Did you mean 'TeamStatsRepository'?
|
|
||||||
apps/api/src/domain/team/TeamProviders.ts(37,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
|
|
||||||
apps/api/src/domain/team/TeamService.test.ts(9,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
|
|
||||||
apps/api/src/domain/team/TeamService.ts(14,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
|
|
||||||
apps/api/src/persistence/achievement/AchievementPersistenceModule.test.ts(1,15): error TS2724: '"@core/identity/domain/repositories/AchievementRepository"' has no exported member named 'IAchievementRepository'. Did you mean 'AchievementRepository'?
|
|
||||||
apps/api/src/persistence/inmemory/InMemoryAchievementPersistenceModule.ts(5,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
|
|
||||||
apps/api/src/persistence/inmemory/InMemoryAnalyticsPersistenceModule.ts(5,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
|
|
||||||
apps/api/src/persistence/inmemory/InMemoryIdentityPersistenceModule.ts(7,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
|
|
||||||
apps/api/src/persistence/inmemory/InMemoryMediaPersistenceModule.ts(5,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
|
|
||||||
apps/api/src/persistence/inmemory/InMemoryMediaPersistenceModule.ts(7,15): error TS2724: '"@core/media/domain/repositories/AvatarGenerationRepository"' has no exported member named 'IAvatarGenerationRepository'. Did you mean 'AvatarGenerationRepository'?
|
|
||||||
apps/api/src/persistence/inmemory/InMemoryMediaPersistenceModule.ts(8,15): error TS2724: '"@core/media/domain/repositories/AvatarRepository"' has no exported member named 'IAvatarRepository'. Did you mean 'AvatarRepository'?
|
|
||||||
apps/api/src/persistence/inmemory/InMemoryMediaPersistenceModule.ts(9,15): error TS2724: '"@core/media/domain/repositories/MediaRepository"' has no exported member named 'IMediaRepository'. Did you mean 'MediaRepository'?
|
|
||||||
apps/api/src/persistence/inmemory/InMemoryNotificationsPersistenceModule.ts(5,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
|
|
||||||
apps/api/src/persistence/inmemory/InMemoryNotificationsPersistenceModule.ts(9,15): error TS2724: '"@core/notifications/domain/repositories/NotificationPreferenceRepository"' has no exported member named 'INotificationPreferenceRepository'. Did you mean 'NotificationPreferenceRepository'?
|
|
||||||
apps/api/src/persistence/inmemory/InMemoryNotificationsPersistenceModule.ts(10,15): error TS2724: '"@core/notifications/domain/repositories/NotificationRepository"' has no exported member named 'INotificationRepository'. Did you mean 'NotificationRepository'?
|
|
||||||
apps/api/src/persistence/inmemory/InMemoryPaymentsPersistenceModule.ts(5,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
|
|
||||||
apps/api/src/persistence/inmemory/InMemoryPaymentsPersistenceModule.ts(8,5): error TS2724: '"@core/payments/domain/repositories/MembershipFeeRepository"' has no exported member named 'IMemberPaymentRepository'. Did you mean 'MemberPaymentRepository'?
|
|
||||||
apps/api/src/persistence/inmemory/InMemoryPaymentsPersistenceModule.ts(9,5): error TS2724: '"@core/payments/domain/repositories/MembershipFeeRepository"' has no exported member named 'IMembershipFeeRepository'. Did you mean 'MembershipFeeRepository'?
|
|
||||||
apps/api/src/persistence/inmemory/InMemoryPaymentsPersistenceModule.ts(11,15): error TS2724: '"@core/payments/domain/repositories/PaymentRepository"' has no exported member named 'IPaymentRepository'. Did you mean 'PaymentRepository'?
|
|
||||||
apps/api/src/persistence/inmemory/InMemoryPaymentsPersistenceModule.ts(12,15): error TS2724: '"@core/payments/domain/repositories/PrizeRepository"' has no exported member named 'IPrizeRepository'. Did you mean 'PrizeRepository'?
|
|
||||||
apps/api/src/persistence/inmemory/InMemoryPaymentsPersistenceModule.ts(13,15): error TS2724: '"@core/payments/domain/repositories/WalletRepository"' has no exported member named 'ITransactionRepository'. Did you mean 'TransactionRepository'?
|
|
||||||
apps/api/src/persistence/inmemory/InMemoryPaymentsPersistenceModule.ts(13,39): error TS2724: '"@core/payments/domain/repositories/WalletRepository"' has no exported member named 'IWalletRepository'. Did you mean 'WalletRepository'?
|
|
||||||
apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts(5,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
|
|
||||||
apps/api/src/persistence/inmemory/InMemorySocialPersistenceModule.ts(5,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
|
|
||||||
apps/api/src/persistence/inmemory/InMemorySocialPersistenceModule.ts(7,15): error TS2724: '"@core/social/domain/repositories/FeedRepository"' has no exported member named 'IFeedRepository'. Did you mean 'FeedRepository'?
|
|
||||||
apps/api/src/persistence/inmemory/InMemorySocialPersistenceModule.ts(8,15): error TS2724: '"@core/social/domain/repositories/SocialGraphRepository"' has no exported member named 'ISocialGraphRepository'. Did you mean 'SocialGraphRepository'?
|
|
||||||
apps/api/src/persistence/postgres/PostgresIdentityPersistenceModule.ts(1,15): error TS2305: Module '"@core/shared/application/UseCaseOutputPort"' has no exported member 'Logger'.
|
|
||||||
apps/api/src/persistence/postgres/PostgresNotificationsPersistenceModule.ts(7,15): error TS2724: '"@core/notifications/domain/repositories/NotificationPreferenceRepository"' has no exported member named 'INotificationPreferenceRepository'. Did you mean 'NotificationPreferenceRepository'?
|
|
||||||
apps/api/src/persistence/postgres/PostgresNotificationsPersistenceModule.ts(8,15): error TS2724: '"@core/notifications/domain/repositories/NotificationRepository"' has no exported member named 'INotificationRepository'. Did you mean 'NotificationRepository'?
|
|
||||||
apps/api/src/persistence/postgres/PostgresNotificationsPersistenceModule.ts(9,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
|
|
||||||
apps/api/src/persistence/postgres/PostgresPaymentsPersistenceModule.ts(7,15): error TS2724: '"@core/payments/domain/repositories/MembershipFeeRepository"' has no exported member named 'IMemberPaymentRepository'. Did you mean 'MemberPaymentRepository'?
|
|
||||||
apps/api/src/persistence/postgres/PostgresPaymentsPersistenceModule.ts(7,41): error TS2724: '"@core/payments/domain/repositories/MembershipFeeRepository"' has no exported member named 'IMembershipFeeRepository'. Did you mean 'MembershipFeeRepository'?
|
|
||||||
apps/api/src/persistence/postgres/PostgresPaymentsPersistenceModule.ts(8,15): error TS2724: '"@core/payments/domain/repositories/PaymentRepository"' has no exported member named 'IPaymentRepository'. Did you mean 'PaymentRepository'?
|
|
||||||
apps/api/src/persistence/postgres/PostgresPaymentsPersistenceModule.ts(9,15): error TS2724: '"@core/payments/domain/repositories/PrizeRepository"' has no exported member named 'IPrizeRepository'. Did you mean 'PrizeRepository'?
|
|
||||||
apps/api/src/persistence/postgres/PostgresPaymentsPersistenceModule.ts(10,15): error TS2724: '"@core/payments/domain/repositories/WalletRepository"' has no exported member named 'ITransactionRepository'. Did you mean 'TransactionRepository'?
|
|
||||||
apps/api/src/persistence/postgres/PostgresPaymentsPersistenceModule.ts(10,39): error TS2724: '"@core/payments/domain/repositories/WalletRepository"' has no exported member named 'IWalletRepository'. Did you mean 'WalletRepository'?
|
|
||||||
apps/api/src/persistence/postgres/PostgresRacingPersistenceModule.ts(116,29): error TS2307: Cannot find module '@core/shared/application/UseCaseOutputPort/Logger' or its corresponding type declarations.
|
|
||||||
apps/api/src/persistence/social/PostgresSocialPersistence.integration.test.ts(13,15): error TS2724: '"@core/social/domain/repositories/FeedRepository"' has no exported member named 'IFeedRepository'. Did you mean 'FeedRepository'?
|
|
||||||
apps/api/src/persistence/social/PostgresSocialPersistence.integration.test.ts(14,15): error TS2724: '"@core/social/domain/repositories/SocialGraphRepository"' has no exported member named 'ISocialGraphRepository'. Did you mean 'SocialGraphRepository'?
|
|
||||||
|
|||||||
Reference in New Issue
Block a user