From 6154d544356806217f3bf477edf54c10bdab2d78 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 19 Jan 2026 14:07:49 +0100 Subject: [PATCH] website refactor --- apps/website/app/sponsor/campaigns/page.tsx | 29 +++-- .../client-wrapper/RosterAdminPage.tsx | 4 + .../achievements/AchievementGrid.tsx | 4 +- .../components/drivers/DriverHeaderPanel.tsx | 16 +-- .../drivers/DriverProfileHeader.tsx | 16 ++- .../components/drivers/DriverTableRow.tsx | 10 +- .../drivers/DriversDirectoryHeader.tsx | 24 ++-- .../components/drivers/FeaturedDriverCard.tsx | 12 +- .../components/drivers/ProfileHero.tsx | 18 ++- .../components/drivers/RatingBadge.tsx | 5 +- .../components/drivers/SafetyRatingBadge.tsx | 5 +- .../errors/ErrorAnalyticsDashboard.tsx | 14 ++- apps/website/components/feed/FeedLayout.tsx | 15 ++- .../components/leagues/JoinRequestItem.tsx | 6 +- .../components/leagues/LeagueActivityFeed.tsx | 16 +-- .../leagues/LeagueReviewSummary.tsx | 9 +- .../components/leagues/LeagueSchedule.tsx | 11 +- .../components/leagues/ReviewProtestModal.tsx | 4 +- .../components/leagues/ScheduleRaceCard.tsx | 10 +- .../leagues/SponsorshipRequestCard.tsx | 7 +- .../components/leagues/StandingsTable.tsx | 29 +++-- .../leagues/StewardingQueuePanel.tsx | 4 +- .../components/leagues/WalletSummaryPanel.tsx | 71 ++++++------ .../components/profile/ProfileHeader.tsx | 14 +-- .../components/races/LatestResultsSidebar.tsx | 6 +- apps/website/components/races/RaceHero.tsx | 10 +- .../components/races/RaceHeroWrapper.tsx | 12 +- .../components/races/RaceListItemWrapper.tsx | 24 +--- .../components/races/RaceResultCard.tsx | 34 +++--- .../races/RaceResultCardWrapper.tsx | 9 +- .../components/races/UpcomingRacesSidebar.tsx | 6 +- .../components/sponsors/PricingTableShell.tsx | 3 +- .../components/sponsors/SlotTemplates.ts | 25 ++-- .../sponsors/SponsorInsightsCard.tsx | 54 ++++----- .../sponsors/SponsorInsightsCardTypes.ts | 7 +- .../components/sponsors/SponsorMetricCard.tsx | 8 +- .../sponsors/SponsorshipCategoryCard.tsx | 12 +- .../components/sponsors/TransactionTable.tsx | 8 +- .../components/teams/SkillLevelSection.tsx | 12 +- .../components/teams/TeamCardWrapper.tsx | 18 +-- .../components/teams/TeamLeaderboardItem.tsx | 18 +-- .../components/teams/TeamLeaderboardPanel.tsx | 16 +-- .../components/teams/TeamMembershipCard.tsx | 6 +- .../components/teams/TeamMembershipGrid.tsx | 4 +- apps/website/components/teams/TeamRoster.tsx | 21 ++-- .../components/teams/TeamRosterItem.tsx | 22 ++-- .../website/hooks/league/useLeagueSchedule.ts | 3 + .../league/useLeagueScheduleAdminPageData.ts | 3 + .../view-data/DriverProfileViewDataBuilder.ts | 22 +++- .../view-data/DriversViewDataBuilder.ts | 7 ++ .../LeagueRosterAdminViewDataBuilder.ts | 3 + .../LeagueSponsorshipsViewDataBuilder.ts | 8 +- .../view-data/LeagueWalletViewDataBuilder.ts | 14 ++- .../view-data/ProfileViewDataBuilder.ts | 49 ++++---- .../SponsorDashboardViewDataBuilder.ts | 22 ++-- .../view-data/TeamDetailViewDataBuilder.ts | 7 +- .../view-data/TeamsViewDataBuilder.ts | 5 + .../lib/display-objects/DurationDisplay.ts | 24 ++++ .../lib/display-objects/FinishDisplay.ts | 24 ++++ .../lib/display-objects/MemoryDisplay.ts | 21 ++++ .../lib/display-objects/NumberDisplay.ts | 13 +++ .../lib/display-objects/PercentDisplay.ts | 25 ++++ .../lib/display-objects/StatusDisplay.ts | 44 +++++++ .../lib/display-objects/WinRateDisplay.ts | 5 + apps/website/lib/services/home/HomeService.ts | 4 +- .../types/view-data/DriverProfileViewData.ts | 15 +++ .../lib/types/view-data/DriversViewData.ts | 5 + .../view-data/LeagueRosterAdminViewData.ts | 2 + .../lib/view-data/SponsorDashboardViewData.ts | 10 +- .../lib/view-data/TeamDetailViewData.ts | 4 +- apps/website/lib/view-data/TeamsViewData.ts | 3 + .../leagues/LeagueSponsorshipsViewData.ts | 2 + .../view-models/LeagueScheduleViewModel.ts | 2 + .../lib/view-models/ProtestViewModel.ts | 6 +- .../lib/view-models/RaceResultViewModel.ts | 22 +++- .../lib/view-models/SponsorshipViewModel.ts | 12 +- .../templates/DriverProfileTemplate.tsx | 21 ++-- apps/website/templates/DriversTemplate.tsx | 11 +- .../templates/LeagueSponsorshipsTemplate.tsx | 2 +- .../templates/LeagueWalletTemplate.tsx | 4 +- apps/website/templates/ProfileTemplate.tsx | 9 +- .../website/templates/RosterAdminTemplate.tsx | 4 +- .../templates/SponsorCampaignsTemplate.tsx | 24 ++-- .../templates/SponsorDashboardTemplate.tsx | 20 ++-- .../templates/SponsorLeagueDetailTemplate.tsx | 35 ++++-- apps/website/templates/TeamDetailTemplate.tsx | 6 +- apps/website/templates/TeamsTemplate.tsx | 3 + my_tsc_output.txt | 108 +----------------- 88 files changed, 755 insertions(+), 566 deletions(-) create mode 100644 apps/website/lib/display-objects/DurationDisplay.ts create mode 100644 apps/website/lib/display-objects/FinishDisplay.ts create mode 100644 apps/website/lib/display-objects/MemoryDisplay.ts create mode 100644 apps/website/lib/display-objects/PercentDisplay.ts create mode 100644 apps/website/lib/display-objects/StatusDisplay.ts diff --git a/apps/website/app/sponsor/campaigns/page.tsx b/apps/website/app/sponsor/campaigns/page.tsx index 72b10937b..4f1475435 100644 --- a/apps/website/app/sponsor/campaigns/page.tsx +++ b/apps/website/app/sponsor/campaigns/page.tsx @@ -1,11 +1,15 @@ 'use client'; import { useSponsorSponsorships } from "@/hooks/sponsor/useSponsorSponsorships"; -import { SponsorCampaignsTemplate, SponsorshipType } from "@/templates/SponsorCampaignsTemplate"; +import { SponsorCampaignsTemplate, SponsorshipType, SponsorCampaignsViewData } from "@/templates/SponsorCampaignsTemplate"; import { Box } from "@/ui/Box"; import { Button } from "@/ui/Button"; import { Text } from "@/ui/Text"; import { useState } from 'react'; +import { CurrencyDisplay } from "@/lib/display-objects/CurrencyDisplay"; +import { NumberDisplay } from "@/lib/display-objects/NumberDisplay"; +import { DateDisplay } from "@/lib/display-objects/DateDisplay"; +import { StatusDisplay } from "@/lib/display-objects/StatusDisplay"; export default function SponsorCampaignsPage() { const [typeFilter, setTypeFilter] = useState('all'); @@ -39,22 +43,33 @@ export default function SponsorCampaignsPage() { } // Calculate stats + const totalInvestment = sponsorshipsData.sponsorships.filter((s: any) => s.status === 'active').reduce((sum: number, s: any) => sum + s.price, 0); + const totalImpressions = sponsorshipsData.sponsorships.reduce((sum: number, s: any) => sum + s.impressions, 0); + const stats = { total: sponsorshipsData.sponsorships.length, active: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'active').length, pending: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'pending_approval').length, approved: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'approved').length, rejected: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'rejected').length, - totalInvestment: sponsorshipsData.sponsorships.filter((s: any) => s.status === 'active').reduce((sum: number, s: any) => sum + s.price, 0), - totalImpressions: sponsorshipsData.sponsorships.reduce((sum: number, s: any) => sum + s.impressions, 0), + formattedTotalInvestment: CurrencyDisplay.format(totalInvestment), + formattedTotalImpressions: NumberDisplay.formatCompact(totalImpressions), }; - const viewData = { - sponsorships: sponsorshipsData.sponsorships as any, - stats, + const sponsorships = sponsorshipsData.sponsorships.map((s: any) => ({ + ...s, + formattedInvestment: CurrencyDisplay.format(s.price), + formattedImpressions: NumberDisplay.format(s.impressions), + formattedStartDate: s.seasonStartDate ? DateDisplay.formatShort(s.seasonStartDate) : undefined, + formattedEndDate: s.seasonEndDate ? DateDisplay.formatShort(s.seasonEndDate) : undefined, + })); + + const viewData: SponsorCampaignsViewData = { + sponsorships, + stats: stats as any, }; - const filteredSponsorships = sponsorshipsData.sponsorships.filter((s: any) => { + const filteredSponsorships = sponsorships.filter((s: any) => { // For now, we only have leagues in the DTO if (typeFilter !== 'all' && typeFilter !== 'leagues') return false; if (searchQuery && !s.leagueName.toLowerCase().includes(searchQuery.toLowerCase())) return false; diff --git a/apps/website/client-wrapper/RosterAdminPage.tsx b/apps/website/client-wrapper/RosterAdminPage.tsx index 1b1fb9291..5deaa0975 100644 --- a/apps/website/client-wrapper/RosterAdminPage.tsx +++ b/apps/website/client-wrapper/RosterAdminPage.tsx @@ -17,6 +17,8 @@ import type { LeagueRosterJoinRequestDTO } from '@/lib/types/generated/LeagueRos import type { LeagueRosterMemberDTO } from '@/lib/types/generated/LeagueRosterMemberDTO'; import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts'; +import { DateDisplay } from '@/lib/display-objects/DateDisplay'; + const ROLE_OPTIONS: MembershipRole[] = ['owner', 'admin', 'steward', 'member']; export function RosterAdminPage({ viewData: initialViewData }: Partial>) { @@ -81,6 +83,7 @@ export function RosterAdminPage({ viewData: initialViewData }: Partial ({ @@ -88,6 +91,7 @@ export function RosterAdminPage({ viewData: initialViewData }: Partial - {AchievementDisplay.formatDate(achievement.earnedAt)} + {achievement.earnedAtLabel} diff --git a/apps/website/components/drivers/DriverHeaderPanel.tsx b/apps/website/components/drivers/DriverHeaderPanel.tsx index ee5b3b0bc..044346aac 100644 --- a/apps/website/components/drivers/DriverHeaderPanel.tsx +++ b/apps/website/components/drivers/DriverHeaderPanel.tsx @@ -9,7 +9,8 @@ interface DriverHeaderPanelProps { avatarUrl?: string; nationality: string; rating: number; - globalRank?: number | null; + ratingLabel: string; + globalRankLabel?: string | null; bio?: string | null; actions?: React.ReactNode; } @@ -19,7 +20,8 @@ export function DriverHeaderPanel({ avatarUrl, nationality, rating, - globalRank, + ratingLabel, + globalRankLabel, bio, actions }: DriverHeaderPanelProps) { @@ -54,8 +56,8 @@ export function DriverHeaderPanel({ rounded="2xl" overflow="hidden" border - borderColor="border-charcoal-outline" - bg="bg-graphite-black" + borderColor="border-charcoal-outline" + bg="bg-graphite-black" flexShrink={0} > {name} - + {nationality} - {globalRank !== undefined && globalRank !== null && ( + {globalRankLabel && ( - Global Rank: #{globalRank} + Global Rank: {globalRankLabel} )} diff --git a/apps/website/components/drivers/DriverProfileHeader.tsx b/apps/website/components/drivers/DriverProfileHeader.tsx index 294d290ed..8bd1fb5df 100644 --- a/apps/website/components/drivers/DriverProfileHeader.tsx +++ b/apps/website/components/drivers/DriverProfileHeader.tsx @@ -14,8 +14,10 @@ interface DriverProfileHeaderProps { avatarUrl?: string | null; nationality: string; rating: number; + ratingLabel: string; safetyRating?: number; - globalRank?: number; + safetyRatingLabel: string; + globalRankLabel?: string; bio?: string | null; friendRequestSent: boolean; onAddFriend: () => void; @@ -26,8 +28,10 @@ export function DriverProfileHeader({ avatarUrl, nationality, rating, + ratingLabel, safetyRating = 92, - globalRank, + safetyRatingLabel, + globalRankLabel, bio, friendRequestSent, onAddFriend, @@ -56,11 +60,11 @@ export function DriverProfileHeader({ {name} - {globalRank && ( + {globalRankLabel && ( - #{globalRank} + {globalRankLabel} )} @@ -72,8 +76,8 @@ export function DriverProfileHeader({ - - + + diff --git a/apps/website/components/drivers/DriverTableRow.tsx b/apps/website/components/drivers/DriverTableRow.tsx index d456fb48f..55d41ebab 100644 --- a/apps/website/components/drivers/DriverTableRow.tsx +++ b/apps/website/components/drivers/DriverTableRow.tsx @@ -13,7 +13,8 @@ interface DriverTableRowProps { avatarUrl?: string | null; nationality: string; rating: number; - wins: number; + ratingLabel: string; + winsLabel: string; onClick: () => void; } @@ -23,7 +24,8 @@ export function DriverTableRow({ avatarUrl, nationality, rating, - wins, + ratingLabel, + winsLabel, onClick, }: DriverTableRowProps) { return ( @@ -58,11 +60,11 @@ export function DriverTableRow({ {nationality} - + - {wins} + {winsLabel} diff --git a/apps/website/components/drivers/DriversDirectoryHeader.tsx b/apps/website/components/drivers/DriversDirectoryHeader.tsx index 555ba4e56..fb29836c9 100644 --- a/apps/website/components/drivers/DriversDirectoryHeader.tsx +++ b/apps/website/components/drivers/DriversDirectoryHeader.tsx @@ -17,25 +17,25 @@ interface DriverStat { } interface DriversDirectoryHeaderProps { - totalDrivers: number; - activeDrivers: number; - totalWins: number; - totalRaces: number; + totalDriversLabel: string; + activeDriversLabel: string; + totalWinsLabel: string; + totalRacesLabel: string; onViewLeaderboard: () => void; } export function DriversDirectoryHeader({ - totalDrivers, - activeDrivers, - totalWins, - totalRaces, + totalDriversLabel, + activeDriversLabel, + totalWinsLabel, + totalRacesLabel, onViewLeaderboard, }: DriversDirectoryHeaderProps) { const stats: DriverStat[] = [ - { label: 'drivers', value: totalDrivers, intent: 'primary' }, - { label: 'active', value: activeDrivers, intent: 'success' }, - { label: 'total wins', value: totalWins.toLocaleString(), intent: 'warning' }, - { label: 'races', value: totalRaces.toLocaleString(), intent: 'telemetry' }, + { label: 'drivers', value: totalDriversLabel, intent: 'primary' }, + { label: 'active', value: activeDriversLabel, intent: 'success' }, + { label: 'total wins', value: totalWinsLabel, intent: 'warning' }, + { label: 'races', value: totalRacesLabel, intent: 'telemetry' }, ]; return ( diff --git a/apps/website/components/drivers/FeaturedDriverCard.tsx b/apps/website/components/drivers/FeaturedDriverCard.tsx index 257372ae6..687aa6250 100644 --- a/apps/website/components/drivers/FeaturedDriverCard.tsx +++ b/apps/website/components/drivers/FeaturedDriverCard.tsx @@ -33,9 +33,9 @@ interface FeaturedDriverCardProps { name: string; nationality: string; avatarUrl?: string; - rating: number; - wins: number; - podiums: number; + ratingLabel: string; + winsLabel: string; + podiumsLabel: string; skillLevel?: string; category?: string; }; @@ -142,17 +142,17 @@ export function FeaturedDriverCard({ driver, position, onClick }: FeaturedDriver diff --git a/apps/website/components/drivers/ProfileHero.tsx b/apps/website/components/drivers/ProfileHero.tsx index 22823e76c..856e72507 100644 --- a/apps/website/components/drivers/ProfileHero.tsx +++ b/apps/website/components/drivers/ProfileHero.tsx @@ -17,12 +17,12 @@ interface ProfileHeroProps { avatarUrl?: string; country: string; iracingId: number; - joinedAt: string | Date; + joinedAtLabel: string; }; stats: { - rating: number; + ratingLabel: string; } | null; - globalRank: number; + globalRankLabel: string; timezone: string; socialHandles: { platform: string; @@ -47,7 +47,7 @@ function getSocialIcon(platform: string) { export function ProfileHero({ driver, stats, - globalRank, + globalRankLabel, timezone, socialHandles, onAddFriend, @@ -87,14 +87,14 @@ export function ProfileHero({ - {stats.rating} + {stats.ratingLabel} Rating - #{globalRank} + {globalRankLabel} Global @@ -111,11 +111,7 @@ export function ProfileHero({ - Joined{' '} - {new Date(driver.joinedAt).toLocaleDateString('en-US', { - month: 'short', - year: 'numeric', - })} + Joined {driver.joinedAtLabel} diff --git a/apps/website/components/drivers/RatingBadge.tsx b/apps/website/components/drivers/RatingBadge.tsx index 9d09e3252..13f1445a4 100644 --- a/apps/website/components/drivers/RatingBadge.tsx +++ b/apps/website/components/drivers/RatingBadge.tsx @@ -3,10 +3,11 @@ import { Badge } from '@/ui/Badge'; interface RatingBadgeProps { rating: number; + ratingLabel: string; size?: 'sm' | 'md' | 'lg'; } -export function RatingBadge({ rating, size = 'md' }: RatingBadgeProps) { +export function RatingBadge({ rating, ratingLabel, size = 'md' }: RatingBadgeProps) { const badgeSize = size === 'lg' ? 'md' : size; const getVariant = (val: number): 'warning' | 'primary' | 'success' | 'default' => { @@ -22,7 +23,7 @@ export function RatingBadge({ rating, size = 'md' }: RatingBadgeProps) { variant={getVariant(rating)} size={badgeSize} > - {rating.toLocaleString()} + {ratingLabel} ); } diff --git a/apps/website/components/drivers/SafetyRatingBadge.tsx b/apps/website/components/drivers/SafetyRatingBadge.tsx index f701a717d..0134a24bf 100644 --- a/apps/website/components/drivers/SafetyRatingBadge.tsx +++ b/apps/website/components/drivers/SafetyRatingBadge.tsx @@ -6,10 +6,11 @@ import { Shield } from 'lucide-react'; interface SafetyRatingBadgeProps { rating: number; + ratingLabel: string; size?: 'sm' | 'md' | 'lg'; } -export function SafetyRatingBadge({ rating, size = 'md' }: SafetyRatingBadgeProps) { +export function SafetyRatingBadge({ rating, ratingLabel, size = 'md' }: SafetyRatingBadgeProps) { const getColor = (r: number) => { if (r >= 90) return 'text-performance-green'; if (r >= 70) return 'text-warning-amber'; @@ -65,7 +66,7 @@ export function SafetyRatingBadge({ rating, size = 'md' }: SafetyRatingBadgeProp font="mono" color={colorClass} > - SR {rating.toFixed(0)} + {ratingLabel} ); diff --git a/apps/website/components/errors/ErrorAnalyticsDashboard.tsx b/apps/website/components/errors/ErrorAnalyticsDashboard.tsx index 27adf8407..9e8f2d4ca 100644 --- a/apps/website/components/errors/ErrorAnalyticsDashboard.tsx +++ b/apps/website/components/errors/ErrorAnalyticsDashboard.tsx @@ -29,6 +29,12 @@ import { } from 'lucide-react'; import { useEffect, useState } from 'react'; +import { DateDisplay } from '@/lib/display-objects/DateDisplay'; +import { DurationDisplay } from '@/lib/display-objects/DurationDisplay'; +import { MemoryDisplay } from '@/lib/display-objects/MemoryDisplay'; +import { PercentDisplay } from '@/lib/display-objects/PercentDisplay'; +import { TimeDisplay } from '@/lib/display-objects/TimeDisplay'; + interface ErrorAnalyticsDashboardProps { /** * Auto-refresh interval in milliseconds @@ -41,16 +47,16 @@ interface ErrorAnalyticsDashboardProps { } function formatDuration(duration: number): string { - return duration.toFixed(2) + 'ms'; + return DurationDisplay.formatMs(duration); } function formatPercentage(value: number, total: number): string { if (total === 0) return '0%'; - return ((value / total) * 100).toFixed(1) + '%'; + return PercentDisplay.format(value / total); } function formatMemory(bytes: number): string { - return (bytes / 1024 / 1024).toFixed(1) + 'MB'; + return MemoryDisplay.formatMB(bytes); } interface PerformanceWithMemory extends Performance { @@ -321,7 +327,7 @@ export function ErrorAnalyticsDashboard({ {error.type} - {new Date(error.timestamp).toLocaleTimeString()} + {DateDisplay.formatTime(error.timestamp)} {error.message} diff --git a/apps/website/components/feed/FeedLayout.tsx b/apps/website/components/feed/FeedLayout.tsx index 114b280a4..f88aed306 100644 --- a/apps/website/components/feed/FeedLayout.tsx +++ b/apps/website/components/feed/FeedLayout.tsx @@ -8,6 +8,7 @@ import { Grid } from '@/ui/Grid'; import { Stack } from '@/ui/Stack'; import { Section } from '@/ui/Section'; import { Text } from '@/ui/Text'; +import { DateDisplay } from '@/lib/display-objects/DateDisplay'; interface FeedItemData { id: string; @@ -46,6 +47,16 @@ export function FeedLayout({ upcomingRaces, latestResults }: FeedLayoutProps) { + const formattedUpcomingRaces = upcomingRaces.map(r => ({ + ...r, + formattedDate: DateDisplay.formatShort(r.scheduledAt), + })); + + const formattedLatestResults = latestResults.map(r => ({ + ...r, + formattedDate: DateDisplay.formatShort(r.scheduledAt), + })); + return (
@@ -64,8 +75,8 @@ export function FeedLayout({ - - + + diff --git a/apps/website/components/leagues/JoinRequestItem.tsx b/apps/website/components/leagues/JoinRequestItem.tsx index 550450c04..3c0ab6cdf 100644 --- a/apps/website/components/leagues/JoinRequestItem.tsx +++ b/apps/website/components/leagues/JoinRequestItem.tsx @@ -4,7 +4,7 @@ import { Text } from '@/ui/Text'; interface JoinRequestItemProps { driverId: string; - requestedAt: string | Date; + formattedRequestedAt: string; onApprove: () => void; onReject: () => void; isApproving?: boolean; @@ -13,7 +13,7 @@ interface JoinRequestItemProps { export function JoinRequestItem({ driverId, - requestedAt, + formattedRequestedAt, onApprove, onReject, isApproving, @@ -47,7 +47,7 @@ export function JoinRequestItem({ {driverId} - Requested {new Date(requestedAt).toLocaleDateString()} + Requested {formattedRequestedAt} diff --git a/apps/website/components/leagues/LeagueActivityFeed.tsx b/apps/website/components/leagues/LeagueActivityFeed.tsx index abdbde462..0f390a3eb 100644 --- a/apps/website/components/leagues/LeagueActivityFeed.tsx +++ b/apps/website/components/leagues/LeagueActivityFeed.tsx @@ -1,6 +1,7 @@ import { ActivityFeedItem } from '@/components/feed/ActivityFeedItem'; import { useLeagueRaces } from "@/hooks/league/useLeagueRaces"; import { LeagueActivityService } from '@/lib/services/league/LeagueActivityService'; +import { RelativeTimeDisplay } from '@/lib/display-objects/RelativeTimeDisplay'; import { Icon } from '@/ui/Icon'; import { Text } from '@/ui/Text'; import { Stack } from '@/ui/Stack'; @@ -20,19 +21,6 @@ interface LeagueActivityFeedProps { limit?: number; } -function timeAgo(timestamp: Date): string { - const diffMs = Date.now() - timestamp.getTime(); - const diffMinutes = Math.floor(diffMs / 60000); - if (diffMinutes < 1) return 'Just now'; - if (diffMinutes < 60) return `${diffMinutes} min ago`; - const diffHours = Math.floor(diffMinutes / 60); - if (diffHours < 24) return `${diffHours}h ago`; - const diffDays = Math.floor(diffHours / 24); - if (diffDays === 1) return 'Yesterday'; - if (diffDays < 7) return `${diffDays}d ago`; - return timestamp.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); -} - export function LeagueActivityFeed({ leagueId, limit = 10 }: LeagueActivityFeedProps) { const { data: raceList = [], isLoading } = useLeagueRaces(leagueId); @@ -140,7 +128,7 @@ function ActivityItem({ activity }: { activity: LeagueActivity }) { ); } diff --git a/apps/website/components/leagues/LeagueReviewSummary.tsx b/apps/website/components/leagues/LeagueReviewSummary.tsx index 4de605070..1daf03fe7 100644 --- a/apps/website/components/leagues/LeagueReviewSummary.tsx +++ b/apps/website/components/leagues/LeagueReviewSummary.tsx @@ -27,6 +27,9 @@ import { Icon } from '@/ui/Icon'; import { Card } from '@/ui/Card'; import { Grid } from '@/ui/Grid'; +import { DateDisplay } from '@/lib/display-objects/DateDisplay'; +import { DurationDisplay } from '@/lib/display-objects/DurationDisplay'; + interface LeagueReviewSummaryProps { form: LeagueConfigFormModel; presets: LeagueScoringPresetViewModel[]; @@ -139,11 +142,7 @@ export function LeagueReviewSummary({ form, presets }: LeagueReviewSummaryProps) const seasonStartLabel = timings.seasonStartDate - ? new Date(timings.seasonStartDate).toLocaleDateString(undefined, { - month: 'short', - day: 'numeric', - year: 'numeric', - }) + ? DateDisplay.formatShort(timings.seasonStartDate) : null; const stewardingLabel = (() => { diff --git a/apps/website/components/leagues/LeagueSchedule.tsx b/apps/website/components/leagues/LeagueSchedule.tsx index a38227834..69928273d 100644 --- a/apps/website/components/leagues/LeagueSchedule.tsx +++ b/apps/website/components/leagues/LeagueSchedule.tsx @@ -194,17 +194,10 @@ export function LeagueSchedule({ leagueId, onRaceClick }: LeagueScheduleProps) { - {race.scheduledAt.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - })} + {race.formattedDate} - {race.scheduledAt.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - })} + {race.formattedTime} {isPast && race.status === 'completed' && ( View Results → diff --git a/apps/website/components/leagues/ReviewProtestModal.tsx b/apps/website/components/leagues/ReviewProtestModal.tsx index d9aa8059f..9388723cc 100644 --- a/apps/website/components/leagues/ReviewProtestModal.tsx +++ b/apps/website/components/leagues/ReviewProtestModal.tsx @@ -286,7 +286,7 @@ export function ReviewProtestModal({ Filed Date - {new Date(protest.filedAt || protest.submittedAt).toLocaleString()} + {protest.formattedSubmittedAt} @@ -299,7 +299,7 @@ export function ReviewProtestModal({ Status - {protest.status} + {protest.statusDisplay} diff --git a/apps/website/components/leagues/ScheduleRaceCard.tsx b/apps/website/components/leagues/ScheduleRaceCard.tsx index 84898bad6..bd74a4fa9 100644 --- a/apps/website/components/leagues/ScheduleRaceCard.tsx +++ b/apps/website/components/leagues/ScheduleRaceCard.tsx @@ -15,8 +15,10 @@ interface Race { name: string; track?: string; car?: string; - scheduledAt: string; + formattedDate: string; + formattedTime: string; status: string; + statusLabel: string; sessionType?: string; isPast?: boolean; } @@ -33,19 +35,19 @@ export function ScheduleRaceCard({ race }: ScheduleRaceCardProps) { {race.name} - {race.status === 'completed' ? 'Completed' : 'Scheduled'} + {race.statusLabel} - {new Date(race.scheduledAt).toLocaleDateString()} + {race.formattedDate} - {new Date(race.scheduledAt).toLocaleTimeString()} + {race.formattedTime} {race.track && ( diff --git a/apps/website/components/leagues/SponsorshipRequestCard.tsx b/apps/website/components/leagues/SponsorshipRequestCard.tsx index 15c9815da..3629a290a 100644 --- a/apps/website/components/leagues/SponsorshipRequestCard.tsx +++ b/apps/website/components/leagues/SponsorshipRequestCard.tsx @@ -11,7 +11,8 @@ interface SponsorshipRequest { id: string; sponsorName: string; status: 'pending' | 'approved' | 'rejected'; - requestedAt: string; + statusLabel: string; + formattedRequestedAt: string; slotName: string; } @@ -57,7 +58,7 @@ export function SponsorshipRequestCard({ request }: SponsorshipRequestCardProps) {request.sponsorName} - {request.status} + {request.statusLabel} @@ -66,7 +67,7 @@ export function SponsorshipRequestCard({ request }: SponsorshipRequestCardProps) - {new Date(request.requestedAt).toLocaleDateString()} + {request.formattedRequestedAt} diff --git a/apps/website/components/leagues/StandingsTable.tsx b/apps/website/components/leagues/StandingsTable.tsx index 134d15940..8420ff524 100644 --- a/apps/website/components/leagues/StandingsTable.tsx +++ b/apps/website/components/leagues/StandingsTable.tsx @@ -55,12 +55,12 @@ interface StandingsTableProps { standings: Array<{ driverId: string; position: number; - totalPoints: number; - racesFinished: number; - racesStarted: number; - avgFinish: number | null; - penaltyPoints: number; - bonusPoints: number; + positionLabel: string; + totalPointsLabel: string; + racesLabel: string; + avgFinishLabel: string; + penaltyPointsLabel: string; + bonusPointsLabel: string; teamName?: string; }>; drivers: Array<{ @@ -508,7 +508,7 @@ export function StandingsTable({ 'text-white' } > - {row.position} + {row.positionLabel} @@ -625,7 +625,7 @@ export function StandingsTable({ {/* Total Points with Hover Action */} - {row.totalPoints} + {row.totalPointsLabel} {isAdmin && canModify && ( - {row.racesFinished} - /{row.racesStarted} + {row.racesLabel} {/* Avg Finish */} - {row.avgFinish !== null ? row.avgFinish.toFixed(1) : '—'} + {row.avgFinishLabel} {/* Penalty */} - 0 ? 'text-red-400' : 'text-gray-500'} weight={row.penaltyPoints > 0 ? 'medium' : 'normal'}> - {row.penaltyPoints > 0 ? `-${row.penaltyPoints}` : '—'} + + {row.penaltyPointsLabel} {/* Bonus */} - - {row.bonusPoints !== 0 ? `+${row.bonusPoints}` : '—'} + + {row.bonusPointsLabel} diff --git a/apps/website/components/leagues/StewardingQueuePanel.tsx b/apps/website/components/leagues/StewardingQueuePanel.tsx index b4a7894f1..93f02980c 100644 --- a/apps/website/components/leagues/StewardingQueuePanel.tsx +++ b/apps/website/components/leagues/StewardingQueuePanel.tsx @@ -13,7 +13,7 @@ interface Protest { protestingDriver: string; accusedDriver: string; description: string; - submittedAt: string; + formattedSubmittedAt: string; status: 'pending' | 'under_review' | 'resolved' | 'rejected'; } @@ -63,7 +63,7 @@ export function StewardingQueuePanel({ protests, onReview }: StewardingQueuePane - {new Date(protest.submittedAt).toLocaleString()} + {protest.formattedSubmittedAt} diff --git a/apps/website/components/leagues/WalletSummaryPanel.tsx b/apps/website/components/leagues/WalletSummaryPanel.tsx index 360e91e19..c65175ca2 100644 --- a/apps/website/components/leagues/WalletSummaryPanel.tsx +++ b/apps/website/components/leagues/WalletSummaryPanel.tsx @@ -9,21 +9,21 @@ import { ArrowDownLeft, ArrowUpRight, History, Wallet } from 'lucide-react'; interface Transaction { id: string; - type: 'credit' | 'debit'; - amount: number; + type: 'deposit' | 'withdrawal' | 'sponsorship' | 'prize'; + formattedAmount: string; description: string; - date: string; + formattedDate: string; } interface WalletSummaryPanelProps { - balance: number; + formattedBalance: string; currency: string; transactions: Transaction[]; onDeposit: () => void; onWithdraw: () => void; } -export function WalletSummaryPanel({ balance, currency, transactions, onDeposit, onWithdraw }: WalletSummaryPanelProps) { +export function WalletSummaryPanel({ formattedBalance, currency, transactions, onDeposit, onWithdraw }: WalletSummaryPanelProps) { return ( @@ -48,7 +48,7 @@ export function WalletSummaryPanel({ balance, currency, transactions, onDeposit, - {balance.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + {formattedBalance} {currency} @@ -87,37 +87,40 @@ export function WalletSummaryPanel({ balance, currency, transactions, onDeposit, No recent transactions. ) : ( - transactions.map((tx) => ( - - - - { + const isCredit = tx.type === 'deposit' || tx.type === 'sponsorship'; + return ( + + + + + + + + {tx.description} + {tx.formattedDate} + + + - - - - {tx.description} - {new Date(tx.date).toLocaleDateString()} - + {tx.formattedAmount} + - - {tx.type === 'credit' ? '+' : '-'}{tx.amount.toFixed(2)} - - - )) + ); + }) )} diff --git a/apps/website/components/profile/ProfileHeader.tsx b/apps/website/components/profile/ProfileHeader.tsx index c0f46fa49..133161e60 100644 --- a/apps/website/components/profile/ProfileHeader.tsx +++ b/apps/website/components/profile/ProfileHeader.tsx @@ -19,12 +19,12 @@ interface ProfileHeaderProps { avatarUrl?: string; country: string; iracingId: number; - joinedAt: string | Date; + joinedAtLabel: string; }; stats: { - rating: number; + ratingLabel: string; } | null; - globalRank: number; + globalRankLabel: string; onAddFriend?: () => void; friendRequestSent?: boolean; isOwnProfile?: boolean; @@ -33,7 +33,7 @@ interface ProfileHeaderProps { export function ProfileHeader({ driver, stats, - globalRank, + globalRankLabel, onAddFriend, friendRequestSent, isOwnProfile, @@ -69,7 +69,7 @@ export function ProfileHeader({ - Joined {new Date(driver.joinedAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })} + Joined {driver.joinedAtLabel} @@ -78,8 +78,8 @@ export function ProfileHeader({ {stats && ( - - + + )} diff --git a/apps/website/components/races/LatestResultsSidebar.tsx b/apps/website/components/races/LatestResultsSidebar.tsx index a9900d4d5..72ce50e98 100644 --- a/apps/website/components/races/LatestResultsSidebar.tsx +++ b/apps/website/components/races/LatestResultsSidebar.tsx @@ -9,7 +9,7 @@ type RaceWithResults = { track: string; car: string; winnerName: string; - scheduledAt: string | Date; + formattedDate: string; }; interface LatestResultsSidebarProps { @@ -28,14 +28,12 @@ export function LatestResultsSidebar({ results }: LatestResultsSidebarProps) { {results.slice(0, 4).map((result) => { - const scheduledAt = typeof result.scheduledAt === 'string' ? new Date(result.scheduledAt) : result.scheduledAt; - return ( ); diff --git a/apps/website/components/races/RaceHero.tsx b/apps/website/components/races/RaceHero.tsx index 39ed89396..dad0e5c61 100644 --- a/apps/website/components/races/RaceHero.tsx +++ b/apps/website/components/races/RaceHero.tsx @@ -10,7 +10,8 @@ import { Calendar, Car, Clock, LucideIcon } from 'lucide-react'; interface RaceHeroProps { track: string; - scheduledAt: string; + formattedDate: string; + formattedTime: string; car: string; status: 'scheduled' | 'running' | 'completed' | 'cancelled'; statusConfig: { @@ -20,9 +21,8 @@ interface RaceHeroProps { }; } -export function RaceHero({ track, scheduledAt, car, status, statusConfig }: RaceHeroProps) { +export function RaceHero({ track, formattedDate, formattedTime, car, status, statusConfig }: RaceHeroProps) { const StatusIcon = statusConfig.icon; - const date = new Date(scheduledAt); return ( @@ -59,11 +59,11 @@ export function RaceHero({ track, scheduledAt, car, status, statusConfig }: Race - {date.toLocaleDateString()} + {formattedDate} - {date.toLocaleTimeString()} + {formattedTime} diff --git a/apps/website/components/races/RaceHeroWrapper.tsx b/apps/website/components/races/RaceHeroWrapper.tsx index 44f15a4c0..59d5691bd 100644 --- a/apps/website/components/races/RaceHeroWrapper.tsx +++ b/apps/website/components/races/RaceHeroWrapper.tsx @@ -2,6 +2,7 @@ import { RaceHero as UiRaceHero } from '@/components/races/RaceHero'; import { LucideIcon } from 'lucide-react'; +import { DateDisplay } from '@/lib/display-objects/DateDisplay'; interface RaceHeroProps { track: string; @@ -17,7 +18,7 @@ interface RaceHeroProps { } export function RaceHero(props: RaceHeroProps) { - const { statusConfig, ...rest } = props; + const { statusConfig, scheduledAt, ...rest } = props; // Map variant to match UI component expectations const mappedConfig: { @@ -30,5 +31,12 @@ export function RaceHero(props: RaceHeroProps) { variant: statusConfig.variant === 'default' ? 'default' : statusConfig.variant }; - return ; + return ( + + ); } diff --git a/apps/website/components/races/RaceListItemWrapper.tsx b/apps/website/components/races/RaceListItemWrapper.tsx index 53331073f..b6e8519d5 100644 --- a/apps/website/components/races/RaceListItemWrapper.tsx +++ b/apps/website/components/races/RaceListItemWrapper.tsx @@ -2,7 +2,8 @@ import { routes } from '@/lib/routing/RouteConfig'; import { RaceListItem as UiRaceListItem } from '@/components/races/RaceListItem'; -import { CheckCircle2, Clock, PlayCircle, XCircle } from 'lucide-react'; +import { DateDisplay } from '@/lib/display-objects/DateDisplay'; +import { StatusDisplay } from '@/lib/display-objects/StatusDisplay'; interface Race { id: string; @@ -26,45 +27,32 @@ export function RaceListItem({ race, onClick }: RaceListItemProps) { scheduled: { iconName: 'Clock', variant: 'primary' as const, - label: 'Scheduled', }, running: { iconName: 'PlayCircle', variant: 'success' as const, - label: 'LIVE', }, completed: { iconName: 'CheckCircle2', variant: 'default' as const, - label: 'Completed', }, cancelled: { iconName: 'XCircle', variant: 'warning' as const, - label: 'Cancelled', }, }; const config = statusConfig[race.status]; - const formatTime = (date: string) => { - return new Date(date).toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit', - }); - }; - - const date = new Date(race.scheduledAt); - return ( void; @@ -25,10 +27,12 @@ export function RaceResultCard({ raceId, track, car, - scheduledAt, + formattedDate, position, - startPosition, - incidents, + positionLabel, + startPositionLabel, + incidentsLabel, + positionsGainedLabel, leagueName, showLeague = true, onClick, @@ -66,7 +70,7 @@ export function RaceResultCard({ border borderColor="border-outline-steel" > - P{position} + {positionLabel} @@ -78,11 +82,7 @@ export function RaceResultCard({ - {new Date(scheduledAt).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - })} + {formattedDate} {showLeague && leagueName && ( {leagueName} @@ -92,16 +92,16 @@ export function RaceResultCard({ - Started P{startPosition} + Started {startPositionLabel} - 2 ? 'text-error-red' : 'text-gray-500'}> - {incidents}x incidents + + {incidentsLabel} - {position < startPosition && ( + {positionsGainedLabel && ( <> - +{startPosition - position} positions + {positionsGainedLabel} )} diff --git a/apps/website/components/races/RaceResultCardWrapper.tsx b/apps/website/components/races/RaceResultCardWrapper.tsx index c9f75f25c..2d0fa9d4e 100644 --- a/apps/website/components/races/RaceResultCardWrapper.tsx +++ b/apps/website/components/races/RaceResultCardWrapper.tsx @@ -2,6 +2,7 @@ import { RaceResultViewModel } from '@/lib/view-models/RaceResultViewModel'; import { RaceResultCard as UiRaceResultCard } from './RaceResultCard'; +import { DateDisplay } from '@/lib/display-objects/DateDisplay'; interface RaceResultCardProps { race: { @@ -28,10 +29,12 @@ export function RaceResultCard({ raceId={race.id} track={race.track} car={race.car} - scheduledAt={race.scheduledAt} + formattedDate={DateDisplay.formatShort(race.scheduledAt)} position={result.position} - startPosition={result.startPosition} - incidents={result.incidents} + positionLabel={result.formattedPosition} + startPositionLabel={result.formattedStartPosition} + incidentsLabel={result.formattedIncidents} + positionsGainedLabel={result.formattedPositionsGained} leagueName={league?.name} showLeague={showLeague} /> diff --git a/apps/website/components/races/UpcomingRacesSidebar.tsx b/apps/website/components/races/UpcomingRacesSidebar.tsx index 9004248dd..8c284779b 100644 --- a/apps/website/components/races/UpcomingRacesSidebar.tsx +++ b/apps/website/components/races/UpcomingRacesSidebar.tsx @@ -8,7 +8,7 @@ type UpcomingRace = { id: string; track: string; car: string; - scheduledAt: string | Date; + formattedDate: string; }; interface UpcomingRacesSidebarProps { @@ -35,14 +35,12 @@ export function UpcomingRacesSidebar({ races }: UpcomingRacesSidebarProps) { {races.slice(0, 4).map((race) => { - const scheduledAt = typeof race.scheduledAt === 'string' ? new Date(race.scheduledAt) : race.scheduledAt; - return ( ); })} diff --git a/apps/website/components/sponsors/PricingTableShell.tsx b/apps/website/components/sponsors/PricingTableShell.tsx index fe4c57cef..d89aa7004 100644 --- a/apps/website/components/sponsors/PricingTableShell.tsx +++ b/apps/website/components/sponsors/PricingTableShell.tsx @@ -8,6 +8,7 @@ export interface PricingTier { id: string; name: string; price: number; + priceLabel: string; period: string; description: string; features: string[]; @@ -69,7 +70,7 @@ export function PricingTableShell({ title, tiers, onSelect, selectedId }: Pricin {tier.name} - ${tier.price} + {tier.priceLabel} /{tier.period} diff --git a/apps/website/components/sponsors/SlotTemplates.ts b/apps/website/components/sponsors/SlotTemplates.ts index 38c96815b..4aca8cf47 100644 --- a/apps/website/components/sponsors/SlotTemplates.ts +++ b/apps/website/components/sponsors/SlotTemplates.ts @@ -1,62 +1,61 @@ export interface SponsorshipSlot { tier: 'main' | 'secondary'; available: boolean; - price: number; - currency?: string; + priceLabel: string; benefits: string[]; } export const SlotTemplates = { - league: (mainAvailable: boolean, secondaryAvailable: number, mainPrice: number, secondaryPrice: number): SponsorshipSlot[] => [ + league: (mainAvailable: boolean, secondaryAvailable: number, mainPriceLabel: string, secondaryPriceLabel: string): SponsorshipSlot[] => [ { tier: 'main', available: mainAvailable, - price: mainPrice, + priceLabel: mainPriceLabel, benefits: ['Hood placement', 'League banner', 'Prominent logo'], }, { tier: 'secondary', available: secondaryAvailable > 0, - price: secondaryPrice, + priceLabel: secondaryPriceLabel, benefits: ['Side logo placement', 'League page listing'], }, { tier: 'secondary', available: secondaryAvailable > 1, - price: secondaryPrice, + priceLabel: secondaryPriceLabel, benefits: ['Side logo placement', 'League page listing'], }, ], - race: (mainAvailable: boolean, mainPrice: number): SponsorshipSlot[] => [ + race: (mainAvailable: boolean, mainPriceLabel: string): SponsorshipSlot[] => [ { tier: 'main', available: mainAvailable, - price: mainPrice, + priceLabel: mainPriceLabel, benefits: ['Race title sponsor', 'Stream overlay', 'Results banner'], }, ], - driver: (available: boolean, price: number): SponsorshipSlot[] => [ + driver: (available: boolean, priceLabel: string): SponsorshipSlot[] => [ { tier: 'main', available, - price, + priceLabel, benefits: ['Suit logo', 'Helmet branding', 'Social mentions'], }, ], - team: (mainAvailable: boolean, secondaryAvailable: boolean, mainPrice: number, secondaryPrice: number): SponsorshipSlot[] => [ + team: (mainAvailable: boolean, secondaryAvailable: boolean, mainPriceLabel: string, secondaryPriceLabel: string): SponsorshipSlot[] => [ { tier: 'main', available: mainAvailable, - price: mainPrice, + priceLabel: mainPriceLabel, benefits: ['Team name suffix', 'Car livery', 'All driver suits'], }, { tier: 'secondary', available: secondaryAvailable, - price: secondaryPrice, + priceLabel: secondaryPriceLabel, benefits: ['Team page logo', 'Minor livery placement'], }, ], diff --git a/apps/website/components/sponsors/SponsorInsightsCard.tsx b/apps/website/components/sponsors/SponsorInsightsCard.tsx index 158955004..8cfd98c96 100644 --- a/apps/website/components/sponsors/SponsorInsightsCard.tsx +++ b/apps/website/components/sponsors/SponsorInsightsCard.tsx @@ -47,11 +47,11 @@ export interface SponsorInsightsProps { slots: SponsorshipSlot[]; additionalStats?: { label: string; - items: Array<{ label: string; value: string | number }>; + items: Array<{ label: string; value: string }>; }; - trustScore?: number; - discordMembers?: number; - monthlyActivity?: number; + trustScoreLabel?: string; + discordMembersLabel?: string; + monthlyActivityLabel?: string; ctaLabel?: string; ctaHref?: string; currentSponsorId?: string; @@ -67,9 +67,9 @@ export function SponsorInsightsCard({ metrics, slots, additionalStats, - trustScore, - discordMembers, - monthlyActivity, + trustScoreLabel, + discordMembersLabel, + monthlyActivityLabel, ctaLabel, ctaHref, currentSponsorId, @@ -111,22 +111,10 @@ export function SponsorInsightsCard({ setError(null); try { - const slot = slotTier === 'main' ? mainSlot : secondarySlots[0]; - const slotPrice = slot?.price ?? 0; - - const request = { - sponsorId: currentSponsorId, - entityType: getSponsorableEntityType(entityType), - entityId, - tier: slotTier, - offeredAmount: slotPrice * 100, - currency: (slot?.currency as 'USD' | 'EUR' | 'GBP') ?? 'USD', - message: `Interested in sponsoring ${entityName} as ${slotTier} sponsor.`, - }; - - console.log('Sponsorship request:', request); - setAppliedTiers(prev => new Set([...prev, slotTier])); + // Note: In a real app, we would fetch the raw price from the API or a ViewModel + // For now, we assume the parent handles the actual request logic onSponsorshipRequested?.(slotTier); + setAppliedTiers(prev => new Set([...prev, slotTier])); } catch (err) { console.error('Failed to apply for sponsorship:', err); @@ -134,7 +122,7 @@ export function SponsorInsightsCard({ } finally { setApplyingTier(null); } - }, [currentSponsorId, ctaHref, entityType, entityId, entityName, onNavigate, mainSlot, secondarySlots, appliedTiers, getSponsorableEntityType, onSponsorshipRequested]); + }, [currentSponsorId, ctaHref, entityType, entityId, onNavigate, appliedTiers, onSponsorshipRequested]); return ( - {(trustScore !== undefined || discordMembers !== undefined || monthlyActivity !== undefined) && ( + {(trustScoreLabel || discordMembersLabel || monthlyActivityLabel) && ( - {trustScore !== undefined && ( + {trustScoreLabel && ( Trust Score: - {trustScore}/100 + {trustScoreLabel} )} - {discordMembers !== undefined && ( + {discordMembersLabel && ( Discord: - {discordMembers.toLocaleString()} + {discordMembersLabel} )} - {monthlyActivity !== undefined && ( + {monthlyActivityLabel && ( Monthly Activity: - {monthlyActivity}% + {monthlyActivityLabel} )} @@ -206,7 +194,7 @@ export function SponsorInsightsCard({ statusColor={mainSlot.available ? 'text-performance-green' : 'text-gray-500'} benefits={mainSlot.benefits.join(' • ')} available={mainSlot.available} - price={`$${mainSlot.price.toLocaleString()}/season`} + price={mainSlot.priceLabel} action={