website refactor

This commit is contained in:
2026-01-19 00:46:46 +01:00
parent b0431637b7
commit e1ce3bffd1
21 changed files with 297 additions and 121 deletions

View File

@@ -14,31 +14,31 @@ interface RaceCardProps {
track: string;
car: string;
scheduledAt: string;
scheduledAtLabel: string;
timeLabel: string;
status: string;
statusLabel: string;
statusVariant: 'primary' | 'success' | 'warning' | 'critical' | 'default' | 'secondary' | 'info' | 'danger';
leagueName: string;
leagueId?: string;
strengthOfField?: number | null;
onClick?: () => void;
statusConfig: {
intent: 'primary' | 'success' | 'warning' | 'critical' | 'default' | 'secondary' | 'info' | 'danger';
icon: LucideIcon | null;
label: string;
};
}
export function RaceCard({
track,
car,
scheduledAt,
scheduledAtLabel,
timeLabel,
status,
statusLabel,
statusVariant,
leagueName,
leagueId,
strengthOfField,
onClick,
statusConfig,
}: RaceCardProps) {
const scheduledAtDate = new Date(scheduledAt);
return (
<Card
variant="dark"
@@ -48,10 +48,10 @@ export function RaceCard({
{/* Time Column */}
<Stack align="center" gap={1}>
<Text size="lg" weight="bold" variant="high">
{scheduledAtDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
{timeLabel}
</Text>
<Text size="xs" variant={statusConfig.intent === 'default' ? 'low' : (statusConfig.intent as any)}>
{status === 'running' ? 'LIVE' : scheduledAtDate.toLocaleDateString()}
<Text size="xs" variant={statusVariant === 'default' ? 'low' : (statusVariant as any)}>
{status === 'running' ? 'LIVE' : scheduledAtLabel}
</Text>
</Stack>
@@ -83,8 +83,8 @@ export function RaceCard({
</Stack>
{/* Status Badge */}
<Badge variant={statusConfig.intent}>
{statusConfig.label}
<Badge variant={statusVariant}>
{statusLabel}
</Badge>
</Stack>

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { LucideIcon } from 'lucide-react';
import { raceStatusConfig } from '@/lib/utilities/raceStatus';
import { RaceStatusDisplay } from '@/lib/display-objects/RaceStatusDisplay';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import { RaceCard as UiRaceCard } from './RaceCard';
interface RaceCardProps {
@@ -18,28 +18,20 @@ interface RaceCardProps {
}
export function RaceCard({ race, onClick }: RaceCardProps) {
const config = raceStatusConfig[race.status as keyof typeof raceStatusConfig] || {
border: 'border-charcoal-outline',
bg: 'bg-charcoal-outline',
color: 'text-gray-400',
icon: null,
label: 'Scheduled',
};
return (
<UiRaceCard
track={race.track}
car={race.car}
scheduledAt={race.scheduledAt}
scheduledAtLabel={DateDisplay.formatShort(race.scheduledAt)}
timeLabel={DateDisplay.formatTime(race.scheduledAt)}
status={race.status}
statusLabel={RaceStatusDisplay.getLabel(race.status)}
statusVariant={RaceStatusDisplay.getVariant(race.status) as any}
leagueName={race.leagueName}
leagueId={race.leagueId}
strengthOfField={race.strengthOfField}
onClick={onClick}
statusConfig={{
...config,
icon: config.icon as LucideIcon | null,
}}
/>
);
}

View File

@@ -19,29 +19,6 @@ interface RaceListProps {
}
export function RaceList({ racesByDate, totalCount, onRaceClick }: RaceListProps) {
const statusConfig = {
scheduled: {
icon: Clock,
variant: 'primary' as const,
label: 'Scheduled',
},
running: {
icon: PlayCircle,
variant: 'success' as const,
label: 'LIVE',
},
completed: {
icon: CheckCircle2,
variant: 'default' as const,
label: 'Completed',
},
cancelled: {
icon: XCircle,
variant: 'warning' as const,
label: 'Cancelled',
},
};
if (racesByDate.length === 0) {
return (
<EmptyState
@@ -62,8 +39,6 @@ export function RaceList({ racesByDate, totalCount, onRaceClick }: RaceListProps
<Stack gap={2}>
{group.races.map((race) => {
const config = statusConfig[race.status as keyof typeof statusConfig] || statusConfig.scheduled;
return (
<RaceListItem
key={race.id}
@@ -76,7 +51,9 @@ export function RaceList({ racesByDate, totalCount, onRaceClick }: RaceListProps
leagueHref={routes.league.detail(race.leagueId ?? '')}
strengthOfField={race.strengthOfField}
onClick={() => onRaceClick(race.id)}
statusConfig={config}
statusLabel={race.statusLabel}
statusVariant={race.statusVariant as any}
statusIconName={race.statusIconName}
/>
);
})}

View File

@@ -5,9 +5,16 @@ import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { Text } from '@/ui/Text';
import { RaceCard, RaceTimeColumn, RaceInfo } from '@/ui/RaceCard';
import { Car, Trophy, Zap, ArrowRight } from 'lucide-react';
import { Car, Trophy, Zap, ArrowRight, Clock, PlayCircle, CheckCircle2, XCircle, HelpCircle } from 'lucide-react';
import React from 'react';
import { LucideIcon } from 'lucide-react';
const ICON_MAP = {
Clock,
PlayCircle,
CheckCircle2,
XCircle,
HelpCircle,
};
interface RaceListItemProps {
track: string;
@@ -17,15 +24,13 @@ interface RaceListItemProps {
dateLabel?: string;
dayLabel?: string;
status: string;
statusLabel: string;
statusVariant: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
statusIconName: string;
leagueName?: string | null;
leagueHref?: string;
strengthOfField?: number | null;
onClick: () => void;
statusConfig: {
icon: LucideIcon;
variant: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
label: string;
};
}
export function RaceListItem({
@@ -36,13 +41,16 @@ export function RaceListItem({
dateLabel,
dayLabel,
status,
statusLabel,
statusVariant,
statusIconName,
leagueName,
leagueHref,
strengthOfField,
onClick,
statusConfig,
}: RaceListItemProps) {
const isLive = status === 'running';
const StatusIcon = ICON_MAP[statusIconName as keyof typeof ICON_MAP] || HelpCircle;
return (
<RaceCard onClick={onClick} isLive={isLive}>
@@ -59,9 +67,9 @@ export function RaceListItem({
title={track}
subtitle={car}
badge={
<Badge variant={statusConfig.variant}>
<Icon icon={statusConfig.icon} size={3.5} />
{statusConfig.label}
<Badge variant={statusVariant}>
<Icon icon={StatusIcon} size={3.5} />
{statusLabel}
</Badge>
}
meta={

View File

@@ -1,19 +1,18 @@
import { RaceSummary } from '@/ui/RaceSummary';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import React from 'react';
interface RaceSummaryItemProps {
track: string;
meta: string;
date: Date;
dateLabel: string;
}
export function RaceSummaryItem({ track, meta, date }: RaceSummaryItemProps) {
export function RaceSummaryItem({ track, meta, dateLabel }: RaceSummaryItemProps) {
return (
<RaceSummary
track={track}
meta={meta}
date={DateDisplay.formatShort(date)}
date={dateLabel}
/>
);
}

View File

@@ -14,7 +14,9 @@ interface TeamDetailsHeaderProps {
description?: string;
logoUrl?: string;
memberCount: number;
memberCountLabel?: string;
foundedDate?: string;
foundedDateLabel?: string;
isAdmin?: boolean;
onAdminClick?: () => void;
}
@@ -25,7 +27,9 @@ export function TeamDetailsHeader({
description,
logoUrl,
memberCount,
memberCountLabel,
foundedDate,
foundedDateLabel,
isAdmin,
onAdminClick,
}: TeamDetailsHeaderProps) {
@@ -38,6 +42,9 @@ export function TeamDetailsHeader({
</div>
}
description={description || 'No mission statement provided.'}
memberCount={memberCount}
memberCountLabel={memberCountLabel}
foundedDateLabel={foundedDateLabel}
sideContent={
<div className="w-32 h-32 bg-[var(--ui-color-bg-surface-muted)] border border-[var(--ui-color-border-default)] flex items-center justify-center overflow-hidden rounded-lg">
{logoUrl ? (
@@ -58,7 +65,7 @@ export function TeamDetailsHeader({
},
{
label: 'Established',
value: foundedDate ? new Date(foundedDate).toLocaleDateString() : 'Unknown',
value: foundedDateLabel || 'Unknown',
}
]}
/>

View File

@@ -17,13 +17,16 @@ interface TeamHeroProps {
description?: string;
category?: string | null;
createdAt?: string;
foundedDateLabel?: string;
leagues: { id: string }[];
};
memberCount: number;
memberCountLabel?: string;
leagueCountLabel?: string;
onUpdate: () => void;
}
export function TeamHero({ team, memberCount, onUpdate }: TeamHeroProps) {
export function TeamHero({ team, memberCount, memberCountLabel, leagueCountLabel, onUpdate }: TeamHeroProps) {
return (
<Card>
<Group align="start" justify="between" wrap gap={6}>
@@ -44,20 +47,20 @@ export function TeamHero({ team, memberCount, onUpdate }: TeamHeroProps) {
stats={[
{
label: 'Personnel',
value: `${memberCount} ${memberCount === 1 ? 'member' : 'members'}`,
value: memberCountLabel || 'Unknown',
},
...(team.category ? [{
label: 'Category',
value: team.category,
intent: 'primary' as const,
}] : []),
...(team.createdAt ? [{
...(team.foundedDateLabel ? [{
label: 'Founded',
value: new Date(team.createdAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' }),
value: team.foundedDateLabel,
}] : []),
...(team.leagues && team.leagues.length > 0 ? [{
label: 'Activity',
value: `${team.leagues.length} ${team.leagues.length === 1 ? 'league' : 'leagues'}`,
value: leagueCountLabel || 'Unknown',
}] : []),
]}
/>

View File

@@ -10,6 +10,7 @@ interface Member {
driverName: string;
role: string;
joinedAt: string;
joinedAtLabel: string;
}
interface TeamMembersTableProps {
@@ -49,7 +50,7 @@ export function TeamMembersTable({ members, isAdmin, onRemoveMember }: TeamMembe
</TableCell>
<TableCell>
<Text size="xs" color="text-gray-500" font="mono">
{new Date(member.joinedAt).toLocaleDateString()}
{member.joinedAtLabel}
</Text>
</TableCell>
<TableCell textAlign="right">

View File

@@ -1,28 +1,25 @@
import type { RacesPageDataDTO } from '@/lib/types/generated/RacesPageDataDTO';
import type { RacesViewData, RaceViewData } from '@/lib/view-data/RacesViewData';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import { RaceStatusDisplay } from '@/lib/display-objects/RaceStatusDisplay';
import { RelativeTimeDisplay } from '@/lib/display-objects/RelativeTimeDisplay';
export class RacesViewDataBuilder {
static build(apiDto: RacesPageDataDTO): RacesViewData {
const now = new Date();
const races = apiDto.races.map((race): RaceViewData => {
const scheduledAt = new Date(race.scheduledAt);
return {
id: race.id,
track: race.track,
car: race.car,
scheduledAt: race.scheduledAt,
scheduledAtLabel: scheduledAt.toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric',
}),
timeLabel: scheduledAt.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
}),
relativeTimeLabel: this.getRelativeTime(scheduledAt),
scheduledAtLabel: DateDisplay.formatShort(race.scheduledAt),
timeLabel: DateDisplay.formatTime(race.scheduledAt),
relativeTimeLabel: RelativeTimeDisplay.format(race.scheduledAt, now),
status: race.status as RaceViewData['status'],
statusLabel: this.getStatusLabel(race.status),
statusLabel: RaceStatusDisplay.getLabel(race.status),
statusVariant: RaceStatusDisplay.getVariant(race.status),
statusIconName: RaceStatusDisplay.getIconName(race.status),
sessionType: 'Race',
leagueId: race.leagueId,
leagueName: race.leagueName,
@@ -69,32 +66,4 @@ export class RacesViewDataBuilder {
racesByDate,
};
}
private static getStatusLabel(status: string): string {
switch (status) {
case 'scheduled': return 'Scheduled';
case 'running': return 'LIVE';
case 'completed': return 'Completed';
case 'cancelled': return 'Cancelled';
default: return status;
}
}
private static getRelativeTime(date: Date): string {
const now = new Date();
const diffMs = date.getTime() - now.getTime();
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffMs < 0) return 'Past';
if (diffHours < 1) return 'Starting soon';
if (diffHours < 24) return `In ${diffHours}h`;
if (diffDays === 1) return 'Tomorrow';
if (diffDays < 7) return `In ${diffDays} days`;
return date.toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric',
});
}
}

View File

@@ -1,5 +1,8 @@
import type { TeamDetailPageDto } from '@/lib/page-queries/TeamDetailPageQuery';
import type { TeamDetailViewData, TeamDetailData, TeamMemberData, SponsorMetric, TeamTab } from '@/lib/view-data/TeamDetailViewData';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
import { MemberDisplay } from '@/lib/display-objects/MemberDisplay';
import { LeagueDisplay } from '@/lib/display-objects/LeagueDisplay';
/**
* TeamDetailViewDataBuilder - Transforms TeamDetailPageDto into ViewData
@@ -15,6 +18,7 @@ export class TeamDetailViewDataBuilder {
ownerId: apiDto.team.ownerId,
leagues: apiDto.team.leagues,
createdAt: apiDto.team.createdAt,
foundedDateLabel: apiDto.team.createdAt ? DateDisplay.formatMonthYear(apiDto.team.createdAt) : 'Unknown',
specialization: apiDto.team.specialization,
region: apiDto.team.region,
languages: apiDto.team.languages,
@@ -28,6 +32,7 @@ export class TeamDetailViewDataBuilder {
driverName: membership.driverName,
role: membership.role,
joinedAt: membership.joinedAt,
joinedAtLabel: DateDisplay.formatShort(membership.joinedAt),
isActive: membership.isActive,
avatarUrl: membership.avatarUrl,
}));
@@ -80,6 +85,8 @@ export class TeamDetailViewDataBuilder {
isAdmin,
teamMetrics,
tabs,
memberCountLabel: MemberDisplay.formatCount(memberships.length),
leagueCountLabel: LeagueDisplay.formatCount(leagueCount),
};
}
}
}

View File

@@ -0,0 +1,36 @@
/**
* CurrencyDisplay
*
* Deterministic currency formatting for display.
* Avoids Intl and toLocaleString to prevent SSR/hydration mismatches.
*/
export class CurrencyDisplay {
/**
* Formats an amount as currency (e.g., "$10.00").
* Default currency is USD.
*/
static format(amount: number, currency: string = 'USD'): string {
const symbol = currency === 'USD' ? '$' : currency + ' ';
const formattedAmount = amount.toFixed(2);
// Add thousands separators
const parts = formattedAmount.split('.');
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return `${symbol}${parts.join('.')}`;
}
/**
* Formats an amount as a compact currency (e.g., "$10").
*/
static formatCompact(amount: number, currency: string = 'USD'): string {
const symbol = currency === 'USD' ? '$' : currency + ' ';
const roundedAmount = Math.round(amount);
// Add thousands separators
const formattedAmount = roundedAmount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return `${symbol}${formattedAmount}`;
}
}

View File

@@ -1,7 +1,38 @@
export class DateDisplay {
/**
* Formats a date as "Jan 18, 2026" using UTC.
*/
static formatShort(date: string | Date): string {
const d = typeof date === 'string' ? new Date(date) : date;
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
return `${months[d.getUTCMonth()]} ${d.getUTCDate()}, ${d.getUTCFullYear()}`;
}
/**
* Formats a date as "Jan 2026" using UTC.
*/
static formatMonthYear(date: string | Date): string {
const d = typeof date === 'string' ? new Date(date) : date;
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return `${months[d.getUTCMonth()]} ${d.getUTCFullYear()}`;
}
/**
* Formats a date as "15:00" using UTC.
*/
static formatTime(date: string | Date): string {
const d = typeof date === 'string' ? new Date(date) : date;
const hours = d.getUTCHours().toString().padStart(2, '0');
const minutes = d.getUTCMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
}
/**
* Formats a date as "Jan 18" using UTC.
*/
static formatMonthDay(date: string | Date): string {
const d = typeof date === 'string' ? new Date(date) : date;
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return `${months[d.getUTCMonth()]} ${d.getUTCDate()}`;
}
}

View File

@@ -0,0 +1,15 @@
/**
* LeagueDisplay
*
* Deterministic display logic for leagues.
*/
export class LeagueDisplay {
/**
* Formats a league count with pluralization.
* Example: 1 -> "1 league", 2 -> "2 leagues"
*/
static formatCount(count: number): string {
return `${count} ${count === 1 ? 'league' : 'leagues'}`;
}
}

View File

@@ -0,0 +1,22 @@
/**
* MemberDisplay
*
* Deterministic display logic for members.
*/
export class MemberDisplay {
/**
* Formats a member count with pluralization.
* Example: 1 -> "1 member", 2 -> "2 members"
*/
static formatCount(count: number): string {
return `${count} ${count === 1 ? 'member' : 'members'}`;
}
/**
* Formats a member count as "Units" (used in some contexts).
*/
static formatUnits(count: number): string {
return `${count} ${count === 1 ? 'Unit' : 'Units'}`;
}
}

View File

@@ -0,0 +1,44 @@
/**
* RaceStatusDisplay
*
* Deterministic display logic for race statuses.
*/
export type RaceStatusVariant = 'info' | 'success' | 'neutral' | 'warning' | 'primary' | 'default';
export class RaceStatusDisplay {
private static readonly CONFIG: Record<string, { variant: RaceStatusVariant; label: string; icon: string }> = {
scheduled: {
variant: 'primary',
label: 'Scheduled',
icon: 'Clock',
},
running: {
variant: 'success',
label: 'LIVE',
icon: 'PlayCircle',
},
completed: {
variant: 'default',
label: 'Completed',
icon: 'CheckCircle2',
},
cancelled: {
variant: 'warning',
label: 'Cancelled',
icon: 'XCircle',
},
};
static getLabel(status: string): string {
return this.CONFIG[status]?.label || status.toUpperCase();
}
static getVariant(status: string): RaceStatusVariant {
return this.CONFIG[status]?.variant || 'neutral';
}
static getIconName(status: string): string {
return this.CONFIG[status]?.icon || 'HelpCircle';
}
}

View File

@@ -1,5 +1,12 @@
import { NumberDisplay } from './NumberDisplay';
export class RatingDisplay {
static format(rating: number): string {
return rating.toString();
/**
* Formats a rating as a rounded number with thousands separators.
* Example: 1234.56 -> "1,235"
*/
static format(rating: number | null | undefined): string {
if (rating === null || rating === undefined) return '—';
return NumberDisplay.format(Math.round(rating));
}
}
}

View File

@@ -0,0 +1,37 @@
/**
* RelativeTimeDisplay
*
* Deterministic relative time formatting.
*/
export class RelativeTimeDisplay {
/**
* Formats a date relative to "now".
* "now" must be passed as an argument for determinism.
*/
static format(date: string | Date, now: Date): string {
const d = typeof date === 'string' ? new Date(date) : date;
const diffMs = d.getTime() - now.getTime();
const diffHours = Math.floor(Math.abs(diffMs) / (1000 * 60 * 60));
const diffDays = Math.floor(Math.abs(diffMs) / (1000 * 60 * 60 * 24));
if (diffMs < 0) {
if (diffHours < 1) return 'Just now';
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays === 1) return 'Yesterday';
if (diffDays < 7) return `${diffDays}d ago`;
return this.formatAbsolute(d);
} else {
if (diffHours < 1) return 'Starting soon';
if (diffHours < 24) return `In ${diffHours}h`;
if (diffDays === 1) return 'Tomorrow';
if (diffDays < 7) return `In ${diffDays} days`;
return this.formatAbsolute(d);
}
}
private static formatAbsolute(date: Date): string {
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return `${months[date.getUTCMonth()]} ${date.getUTCDate()}`;
}
}

View File

@@ -8,6 +8,8 @@ export interface RaceViewData {
relativeTimeLabel: string;
status: 'scheduled' | 'running' | 'completed' | 'cancelled';
statusLabel: string;
statusVariant: string;
statusIconName: string;
sessionType: string;
leagueId: string | null;
leagueName: string | null;

View File

@@ -22,6 +22,7 @@ export interface TeamDetailData {
ownerId: string;
leagues: string[];
createdAt?: string;
foundedDateLabel?: string;
specialization?: string;
region?: string;
languages?: string[];
@@ -39,6 +40,7 @@ export interface TeamMemberData {
driverName: string;
role: 'owner' | 'manager' | 'member';
joinedAt: string;
joinedAtLabel: string;
isActive: boolean;
avatarUrl: string;
}
@@ -56,4 +58,6 @@ export interface TeamDetailViewData {
isAdmin: boolean;
teamMetrics: SponsorMetric[];
tabs: TeamTab[];
memberCountLabel: string;
leagueCountLabel: string;
}

View File

@@ -109,7 +109,9 @@ export function TeamDetailTemplate({
tag={team.tag}
description={team.description}
memberCount={viewData.memberships.length}
memberCountLabel={viewData.memberCountLabel}
foundedDate={team.createdAt}
foundedDateLabel={team.foundedDateLabel}
isAdmin={viewData.isAdmin}
onAdminClick={() => onTabChange('admin')}
/>

View File

@@ -196,6 +196,19 @@ The following patterns were identified in `apps/website/components` and SHOULD b
### 5. Pluralization
- **Member Count:** `${count} ${count === 1 ? 'member' : 'members'}``MemberDisplay.formatCount(count)`
- **League Count:** `${count} ${count === 1 ? 'league' : 'leagues'}``LeagueDisplay.formatCount(count)`
---
## Existing Display Objects
- **[`DateDisplay`](apps/website/lib/display-objects/DateDisplay.ts)**: UTC-based date and time formatting.
- **[`CurrencyDisplay`](apps/website/lib/display-objects/CurrencyDisplay.ts)**: Deterministic currency formatting.
- **[`RaceStatusDisplay`](apps/website/lib/display-objects/RaceStatusDisplay.ts)**: Race status labels, variants, and icons.
- **[`RatingDisplay`](apps/website/lib/display-objects/RatingDisplay.ts)**: Rounded rating formatting with thousands separators.
- **[`RelativeTimeDisplay`](apps/website/lib/display-objects/RelativeTimeDisplay.ts)**: Deterministic relative time (requires "now" argument).
- **[`MemberDisplay`](apps/website/lib/display-objects/MemberDisplay.ts)**: Member count pluralization.
- **[`LeagueDisplay`](apps/website/lib/display-objects/LeagueDisplay.ts)**: League count pluralization.
---