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

@@ -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()}`;
}
}