website refactor
This commit is contained in:
36
apps/website/lib/display-objects/CurrencyDisplay.ts
Normal file
36
apps/website/lib/display-objects/CurrencyDisplay.ts
Normal 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}`;
|
||||
}
|
||||
}
|
||||
@@ -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()}`;
|
||||
}
|
||||
}
|
||||
|
||||
15
apps/website/lib/display-objects/LeagueDisplay.ts
Normal file
15
apps/website/lib/display-objects/LeagueDisplay.ts
Normal 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'}`;
|
||||
}
|
||||
}
|
||||
22
apps/website/lib/display-objects/MemberDisplay.ts
Normal file
22
apps/website/lib/display-objects/MemberDisplay.ts
Normal 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'}`;
|
||||
}
|
||||
}
|
||||
44
apps/website/lib/display-objects/RaceStatusDisplay.ts
Normal file
44
apps/website/lib/display-objects/RaceStatusDisplay.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
apps/website/lib/display-objects/RelativeTimeDisplay.ts
Normal file
37
apps/website/lib/display-objects/RelativeTimeDisplay.ts
Normal 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()}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user