website refactor
This commit is contained in:
@@ -45,6 +45,7 @@ const cleanErrorHandling = require('./clean-error-handling');
|
|||||||
const servicesImplementContract = require('./services-implement-contract');
|
const servicesImplementContract = require('./services-implement-contract');
|
||||||
const serverActionsReturnResult = require('./server-actions-return-result');
|
const serverActionsReturnResult = require('./server-actions-return-result');
|
||||||
const serverActionsInterface = require('./server-actions-interface');
|
const serverActionsInterface = require('./server-actions-interface');
|
||||||
|
const noDisplayObjectsInUi = require('./no-display-objects-in-ui');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
rules: {
|
rules: {
|
||||||
@@ -78,6 +79,7 @@ module.exports = {
|
|||||||
// Display Object Rules
|
// Display Object Rules
|
||||||
'display-no-domain-models': displayObjectRules['no-io-in-display-objects'],
|
'display-no-domain-models': displayObjectRules['no-io-in-display-objects'],
|
||||||
'display-no-business-logic': displayObjectRules['no-non-class-display-exports'],
|
'display-no-business-logic': displayObjectRules['no-non-class-display-exports'],
|
||||||
|
'no-display-objects-in-ui': noDisplayObjectsInUi,
|
||||||
|
|
||||||
// Page Query Rules
|
// Page Query Rules
|
||||||
'page-query-no-null-returns': pageQueryRules['no-null-returns-in-page-queries'],
|
'page-query-no-null-returns': pageQueryRules['no-null-returns-in-page-queries'],
|
||||||
@@ -207,6 +209,7 @@ module.exports = {
|
|||||||
// Display Objects
|
// Display Objects
|
||||||
'gridpilot-rules/display-no-domain-models': 'error',
|
'gridpilot-rules/display-no-domain-models': 'error',
|
||||||
'gridpilot-rules/display-no-business-logic': 'error',
|
'gridpilot-rules/display-no-business-logic': 'error',
|
||||||
|
'gridpilot-rules/no-display-objects-in-ui': 'error',
|
||||||
|
|
||||||
// Page Queries
|
// Page Queries
|
||||||
'gridpilot-rules/page-query-no-null-returns': 'error',
|
'gridpilot-rules/page-query-no-null-returns': 'error',
|
||||||
|
|||||||
40
apps/website/eslint-rules/no-display-objects-in-ui.js
Normal file
40
apps/website/eslint-rules/no-display-objects-in-ui.js
Normal file
@@ -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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -144,9 +144,11 @@ Additional strict rules:
|
|||||||
|
|
||||||
- View Models SHOULD compose Displays.
|
- View Models SHOULD compose Displays.
|
||||||
- ViewData Builders SHOULD use Displays for all formatting.
|
- 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.
|
- 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
|
## Summary
|
||||||
|
|
||||||
- Displays encapsulate **how something looks** (the single source of truth for formatting logic).
|
- Displays encapsulate **how something looks** (the single source of truth for formatting logic).
|
||||||
|
|||||||
Reference in New Issue
Block a user