230 lines
9.2 KiB
Markdown
230 lines
9.2 KiB
Markdown
# Displays
|
|
|
|
## Definition
|
|
|
|
A **Display** encapsulates **reusable, UI-only display logic**.
|
|
|
|
In this codebase, a Display is a **Frontend Value Object**:
|
|
|
|
- class-based
|
|
- immutable
|
|
- deterministic
|
|
- side-effect free
|
|
|
|
### Distinction from Domain Value Objects
|
|
|
|
While both are "Value Objects", they serve different layers:
|
|
|
|
1. **Domain Value Objects (Core):** Encapsulate business truth and invariants (e.g., `Money`, `EmailAddress`). They are pure and never contain formatting logic.
|
|
2. **Display Objects (Website):** Encapsulate presentation truth and formatting (e.g., `PriceDisplay`, `DateDisplay`). They are used to transform raw data or Domain Value Objects into user-ready strings.
|
|
|
|
It answers the question:
|
|
|
|
> “How should this specific piece of information be shown?”
|
|
|
|
Displays are **not screen-specific**.
|
|
They exist to avoid duplicating presentation logic across View Models.
|
|
|
|
**Naming Convention:**
|
|
- Displays MUST end with `Display` suffix
|
|
- Displays MUST be reusable across multiple screens
|
|
- Valid examples: `PriceDisplay`, `EmailDisplay`, `RatingDisplay`
|
|
- Invalid examples: `DashboardRatingDisplay`, `UserProfileDisplay`
|
|
|
|
---
|
|
|
|
## Responsibilities
|
|
|
|
A Display MAY:
|
|
|
|
- format values (money, dates, durations)
|
|
- handle localization only when localization inputs are deterministic (for example: mapping stable codes to stable labels)
|
|
- map codes to labels
|
|
- encapsulate UI display conventions
|
|
- be reused across multiple View Models
|
|
|
|
In addition, a Display MAY:
|
|
|
|
- normalize presentation inputs (for example trimming/casing)
|
|
- expose multiple explicit display variants (for example `shortLabel`, `longLabel`)
|
|
|
|
A Display MUST:
|
|
|
|
- be deterministic
|
|
- be side-effect free
|
|
- operate only on presentation data
|
|
|
|
A Display MUST:
|
|
|
|
- be implemented as a **class** with a small, explicit API
|
|
- accept only primitives/plain data in its constructor (or static factory)
|
|
- expose only primitive outputs (strings/numbers/booleans)
|
|
|
|
---
|
|
|
|
## Restrictions
|
|
|
|
A Display MUST NOT:
|
|
|
|
- contain business logic
|
|
- enforce domain invariants
|
|
- perform validation
|
|
- influence system behavior
|
|
- be sent back to the server
|
|
- depend on backend or infrastructure concerns
|
|
- **depend on environment-specific APIs** (e.g., `window`, `document`, `navigator`)
|
|
- **be serialized** (they are classes; only their primitive outputs are stored in `ViewData`)
|
|
|
|
In this repository, a Display MUST NOT:
|
|
|
|
- call `Intl.*`
|
|
- call `Date.toLocaleString()` / `Date.toLocaleDateString()` / `Date.toLocaleTimeString()`
|
|
|
|
Reason: these are runtime-locale/timezone dependent and cause SSR/hydration mismatches.
|
|
|
|
### Handling Client-Only Formatting
|
|
|
|
If a formatting requirement **strictly requires** client-only APIs (e.g., browser-native relative time or local timezone detection):
|
|
|
|
1. It MUST NOT live in a `Display Object`.
|
|
2. It SHOULD live in a **View Model** (which is client-only).
|
|
3. The Template should handle the transition from server-provided `ViewData` to client-updated `ViewData`.
|
|
|
|
### Best Practices for Time/Date Formatting
|
|
|
|
To avoid hydration mismatches while still providing good SEO and UX:
|
|
|
|
1. **Use UTC methods for Determinism:** In `Display Objects`, prefer `getUTCDate()`, `getUTCMonth()`, etc., over their local counterparts. This ensures the server and client produce the exact same string regardless of their local timezones.
|
|
2. **Hardcoded Arrays:** Use hardcoded arrays for month/day names instead of `Intl` to ensure consistency across environments.
|
|
3. **Pass "Now" as an Argument:** For relative time (e.g., "time ago"), pass the reference "now" timestamp as an argument to the `Display Object` instead of calling `Date.now()` inside it.
|
|
4. **The "Upgrade" Pattern:**
|
|
- **Server:** `ViewData Builder` uses a `Display Object` to produce a deterministic UTC-based string (e.g., "2024-01-18 15:00 UTC").
|
|
- **Client:** `View Model` uses client APIs (`Intl`, `toLocale*`) to produce a localized string (e.g., "3:00 PM") and updates the `ViewData`.
|
|
|
|
## Localization rule (strict)
|
|
|
|
Localization MUST NOT depend on runtime locale APIs.
|
|
|
|
Allowed approaches:
|
|
|
|
- API returns the exact labels/strings for the current user context.
|
|
- Website maps stable codes to stable labels using a deterministic table.
|
|
|
|
Forbidden approaches:
|
|
|
|
- any usage of `Intl.*`
|
|
- any usage of `toLocale*`
|
|
|
|
If a rule affects system correctness or persistence,
|
|
it does not belong in a Display.
|
|
|
|
---
|
|
|
|
## Ownership & Placement
|
|
|
|
- Displays belong to the **presentation layer**
|
|
- They are frontend-only
|
|
- They are not shared with the backend or core
|
|
|
|
Placement rule (strict):
|
|
|
|
- Displays live under `apps/website/lib/display-objects/*`.
|
|
- Filenames MUST match the class name with `.tsx` extension (e.g., `RatingDisplay.tsx` contains `class RatingDisplay`)
|
|
|
|
---
|
|
|
|
## Relationship to View Models and ViewData Builders
|
|
|
|
Displays are the **shared source of truth** for formatting logic across the website:
|
|
|
|
- **ViewData Builders (Server):** Use Displays to produce deterministic, formatted strings for SEO and initial SSR.
|
|
- **View Models (Client):** Use Displays to produce formatted strings for interactive UI and client-specific context.
|
|
|
|
Additional strict rules:
|
|
|
|
- View Models SHOULD compose Displays.
|
|
- ViewData Builders SHOULD use Displays for all formatting.
|
|
- **Templates and Components MUST NOT use Displays directly.** They must receive already-formatted primitive outputs (strings, numbers) via their props.
|
|
|
|
Reason: This keeps the rendering layer "dumb" and ensures that the `ViewData` remains the single source of truth for what is displayed on the screen.
|
|
|
|
- Displays MUST NOT be serialized or passed across boundaries.
|
|
|
|
---
|
|
|
|
## Testing
|
|
|
|
Displays SHOULD be tested because they often contain:
|
|
|
|
- locale-specific behavior
|
|
- formatting rules
|
|
- edge cases visible to users
|
|
|
|
Additionally:
|
|
|
|
- test determinism by running the same inputs under Node and browser contexts (where applicable)
|
|
- test boundary rules (no `Intl.*`, no `toLocale*`)
|
|
|
|
---
|
|
|
|
## Common Candidates (Found in Components)
|
|
|
|
The following patterns were identified in `apps/website/components` and SHOULD be migrated to Display Objects:
|
|
|
|
### 1. Date & Time
|
|
- **Month/Year:** `new Date().toLocaleDateString('en-US', { month: 'short', year: 'numeric' })` → `DateDisplay.formatMonthYear()`
|
|
- **Time only:** `new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })` → `DateDisplay.formatTime()`
|
|
- **Full Date:** `new Date().toLocaleDateString()` → `DateDisplay.formatShort()` (ensure UTC)
|
|
- **Relative Time:** `timeAgo(timestamp)` logic → `RelativeTimeDisplay.format(timestamp, now)`
|
|
|
|
### 2. Currency & Prices
|
|
- **Price with Symbol:** `$` + `amount.toFixed(2)` → `CurrencyDisplay.format(amount, 'USD')`
|
|
- **Compact Price:** `$` + `amount.toLocaleString()` → `CurrencyDisplay.formatCompact(amount)`
|
|
|
|
### 3. Numbers & Stats
|
|
- **Ratings:** `Math.round(rating).toLocaleString()` → `RatingDisplay.format(rating)`
|
|
- **Percentages:** `(val * 100).toFixed(1) + '%'` → `PercentDisplay.format(val)`
|
|
- **Consistency:** `${stats.consistency}%` → `ConsistencyDisplay.format(stats.consistency)`
|
|
- **Average Finish:** `avgFinish.toFixed(1)` → `FinishDisplay.format(avgFinish)`
|
|
- **Durations:** `duration.toFixed(2) + 'ms'` or `minutes:seconds` → `DurationDisplay.format(ms)`
|
|
- **Memory:** `(bytes / 1024 / 1024).toFixed(1) + 'MB'` → `MemoryDisplay.format(bytes)`
|
|
|
|
### 4. Status & Labels
|
|
- **Race Status:** Mapping `scheduled | running | completed` to labels → `RaceStatusDisplay`
|
|
- **Protest Status:** Mapping `pending | under_review | resolved` to labels → `ProtestStatusDisplay`
|
|
- **Action Status:** Mapping `PENDING | COMPLETED | FAILED` to labels → `ActionStatusDisplay`
|
|
|
|
### 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.
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
- Displays encapsulate **how something looks** (the single source of truth for formatting logic).
|
|
- View Models encapsulate **what a screen needs** (including client-specific "last mile" formatting).
|
|
- Both are presentation concerns.
|
|
- Neither contains business truth.
|
|
|
|
In one sentence: **Displays are the shared source of truth for deterministic formatting logic, used by both the server and the client.**
|
|
|
|
---
|
|
|
|
## Final Rule: Where does formatting go?
|
|
|
|
1. **Is it deterministic?** (e.g., currency symbols, fixed date formats, labels) → **Display Object**.
|
|
2. **Is it client-only?** (e.g., `Intl.*`, `toLocale*`, browser timezone) → **View Model**.
|
|
3. **Is it for SEO?** → **ViewData Builder** (using a Display Object). |