From b0431637b7bcb84759e7e085e81c8253c99f8e23 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 19 Jan 2026 00:35:55 +0100 Subject: [PATCH] website refactor --- apps/website/eslint-rules/index.js | 3 ++ .../eslint-rules/no-display-objects-in-ui.js | 40 +++++++++++++++++++ docs/architecture/website/DISPLAY_OBJECTS.md | 38 +++++++++++++++++- 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 apps/website/eslint-rules/no-display-objects-in-ui.js diff --git a/apps/website/eslint-rules/index.js b/apps/website/eslint-rules/index.js index 44d5d3612..6ce48bdd5 100644 --- a/apps/website/eslint-rules/index.js +++ b/apps/website/eslint-rules/index.js @@ -45,6 +45,7 @@ const cleanErrorHandling = require('./clean-error-handling'); const servicesImplementContract = require('./services-implement-contract'); const serverActionsReturnResult = require('./server-actions-return-result'); const serverActionsInterface = require('./server-actions-interface'); +const noDisplayObjectsInUi = require('./no-display-objects-in-ui'); module.exports = { rules: { @@ -78,6 +79,7 @@ module.exports = { // Display Object Rules 'display-no-domain-models': displayObjectRules['no-io-in-display-objects'], 'display-no-business-logic': displayObjectRules['no-non-class-display-exports'], + 'no-display-objects-in-ui': noDisplayObjectsInUi, // Page Query Rules 'page-query-no-null-returns': pageQueryRules['no-null-returns-in-page-queries'], @@ -207,6 +209,7 @@ module.exports = { // Display Objects 'gridpilot-rules/display-no-domain-models': 'error', 'gridpilot-rules/display-no-business-logic': 'error', + 'gridpilot-rules/no-display-objects-in-ui': 'error', // Page Queries 'gridpilot-rules/page-query-no-null-returns': 'error', diff --git a/apps/website/eslint-rules/no-display-objects-in-ui.js b/apps/website/eslint-rules/no-display-objects-in-ui.js new file mode 100644 index 000000000..3b42cfae4 --- /dev/null +++ b/apps/website/eslint-rules/no-display-objects-in-ui.js @@ -0,0 +1,40 @@ +/** + * ESLint rule: Forbid DisplayObject imports in components and templates + * + * Architecture: + * - DisplayObjects are for Builders and ViewModels + * - Components and Templates must receive already-formatted data + */ + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Forbid DisplayObject imports in components and templates', + category: 'Architecture', + recommended: true, + }, + messages: { + noDisplayObjectsInUi: 'DisplayObjects cannot be used in components or templates. Use ViewData Builders or View Models to format data before passing it to the UI. See docs/architecture/website/DISPLAY_OBJECTS.md', + }, + }, + + create(context) { + const filename = context.getFilename(); + const isUiFile = filename.includes('/components/') || filename.includes('/templates/'); + + if (!isUiFile) return {}; + + return { + ImportDeclaration(node) { + const importPath = node.source.value; + if (importPath.includes('/lib/display-objects/')) { + context.report({ + node, + messageId: 'noDisplayObjectsInUi', + }); + } + }, + }; + }, +}; diff --git a/docs/architecture/website/DISPLAY_OBJECTS.md b/docs/architecture/website/DISPLAY_OBJECTS.md index 523bb172b..68206d8a2 100644 --- a/docs/architecture/website/DISPLAY_OBJECTS.md +++ b/docs/architecture/website/DISPLAY_OBJECTS.md @@ -144,9 +144,11 @@ 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. - - They must not appear in server-to-client DTOs. - - Templates should receive primitive display outputs, not Display instances. --- @@ -165,6 +167,38 @@ Additionally: --- +## 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)` + +--- + ## Summary - Displays encapsulate **how something looks** (the single source of truth for formatting logic).