website refactor
This commit is contained in:
400
docs/architecture/website/COMPONENT_ARCHITECTURE.md
Normal file
400
docs/architecture/website/COMPONENT_ARCHITECTURE.md
Normal file
@@ -0,0 +1,400 @@
|
||||
# Component Architecture
|
||||
|
||||
This document defines the strict separation of concerns for all UI code in the website layer.
|
||||
|
||||
## The Three Layers
|
||||
|
||||
```
|
||||
apps/website/
|
||||
├── app/ ← App Layer (Pages & Layouts)
|
||||
├── components/ ← Component Layer (App Components)
|
||||
├── ui/ ← UI Layer (Pure Elements)
|
||||
└── hooks/ ← Shared Logic
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. App Layer (`app/`)
|
||||
|
||||
**Purpose**: Pages, layouts, and routing configuration.
|
||||
|
||||
**Characteristics**:
|
||||
- ✅ Only page.tsx, layout.tsx, route.tsx files
|
||||
- ✅ Can import from `components/`, `ui/`, `hooks/`
|
||||
- ✅ Can use Next.js features (redirect, cookies, headers)
|
||||
- ✅ Can use Server Components
|
||||
- ❌ **NO raw HTML with styling**
|
||||
- ❌ **NO business logic**
|
||||
- ❌ **NO state management**
|
||||
|
||||
**Allowed**:
|
||||
```typescript
|
||||
// app/dashboard/page.tsx
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<DashboardHeader />
|
||||
<DashboardStats />
|
||||
<DashboardActions />
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Forbidden**:
|
||||
```typescript
|
||||
// ❌ WRONG - Raw HTML in app/
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<div className="bg-white p-6"> {/* ❌ No raw HTML with styling */}
|
||||
<h1 className="text-2xl">Dashboard</h1>
|
||||
<button onClick={handleSubmit}>Click</button> {/* ❌ No inline handlers */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Component Layer (`components/`)
|
||||
|
||||
**Purpose**: App-level components that may contain state and business logic.
|
||||
|
||||
**Characteristics**:
|
||||
- ✅ Can be stateful (useState, useReducer)
|
||||
- ✅ Can have side effects (useEffect)
|
||||
- ✅ Can use hooks
|
||||
- ✅ Can contain business logic
|
||||
- ✅ Can import from `ui/`, `hooks/`
|
||||
- ❌ **NO Next.js imports** (navigation, routing)
|
||||
- ❌ **NO raw HTML with styling** (use ui/ elements)
|
||||
|
||||
**Allowed**:
|
||||
```typescript
|
||||
// components/DashboardHeader.tsx
|
||||
export function DashboardHeader() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<header>
|
||||
<Button onClick={() => setIsOpen(!isOpen)}>Toggle</Button>
|
||||
{isOpen && <Menu />}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Forbidden**:
|
||||
```typescript
|
||||
// ❌ WRONG - Next.js imports in components/
|
||||
import { useRouter } from 'next/navigation'; // ❌
|
||||
|
||||
export function DashboardHeader() {
|
||||
const router = useRouter(); // ❌
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. UI Layer (`ui/`)
|
||||
|
||||
**Purpose**: Pure, reusable, stateless UI elements.
|
||||
|
||||
**Characteristics**:
|
||||
- ✅ Stateless (no useState, useReducer)
|
||||
- ✅ No side effects (no useEffect)
|
||||
- ✅ Pure functions based on props
|
||||
- ✅ Maximum reusability
|
||||
- ✅ Framework-agnostic
|
||||
- ❌ **NO state management**
|
||||
- ❌ **NO Next.js imports**
|
||||
- ❌ **NO business logic**
|
||||
|
||||
**Allowed**:
|
||||
```typescript
|
||||
// ui/Button.tsx
|
||||
export function Button({ children, onClick, variant = 'primary' }) {
|
||||
const className = variant === 'primary'
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 text-black';
|
||||
|
||||
return (
|
||||
<button className={className} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ui/Card.tsx
|
||||
export function Card({ children, className = '' }) {
|
||||
return (
|
||||
<div className={`bg-white rounded-lg shadow ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Forbidden**:
|
||||
```typescript
|
||||
// ❌ WRONG - State in UI element
|
||||
export function Button({ children }) {
|
||||
const [isLoading, setIsLoading] = useState(false); // ❌
|
||||
|
||||
return <button>{isLoading ? 'Loading...' : children}</button>;
|
||||
}
|
||||
|
||||
// ❌ WRONG - Next.js imports
|
||||
import { useRouter } from 'next/navigation'; // ❌
|
||||
|
||||
export function Link({ href, children }) {
|
||||
const router = useRouter(); // ❌
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Hooks Layer (`hooks/`)
|
||||
|
||||
**Purpose**: Shared stateful logic.
|
||||
|
||||
**Characteristics**:
|
||||
- ✅ Can use all hooks
|
||||
- ✅ Can contain business logic
|
||||
- ✅ Can be used by components and pages
|
||||
- ❌ **NO JSX**
|
||||
- ❌ **NO UI rendering**
|
||||
|
||||
**Allowed**:
|
||||
```typescript
|
||||
// hooks/useDropdown.ts
|
||||
export function useDropdown() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
open: () => setIsOpen(true),
|
||||
close: () => setIsOpen(false),
|
||||
toggle: () => setIsOpen(!isOpen),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ESLint Enforcement
|
||||
|
||||
### 1. **No Raw HTML in `app/`**
|
||||
```json
|
||||
{
|
||||
"files": ["app/**/*.tsx", "app/**/*.ts"],
|
||||
"rules": {
|
||||
"gridpilot-rules/no-raw-html-in-app": "error"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Catches**:
|
||||
- `<div className="...">` in app/
|
||||
- `<button style={{...}}>` in app/
|
||||
- `<span onClick={...}>` in app/
|
||||
|
||||
### 2. **UI Element Purity**
|
||||
```json
|
||||
{
|
||||
"files": ["ui/**/*.tsx", "ui/**/*.ts"],
|
||||
"rules": {
|
||||
"gridpilot-rules/ui-element-purity": "error"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Catches**:
|
||||
- `useState` in ui/
|
||||
- `useEffect` in ui/
|
||||
- `useContext` in ui/
|
||||
|
||||
### 3. **No Next.js in UI/Components**
|
||||
```json
|
||||
{
|
||||
"files": ["ui/**/*", "components/**/*"],
|
||||
"rules": {
|
||||
"gridpilot-rules/no-nextjs-imports-in-ui": "error"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Catches**:
|
||||
- `import { useRouter } from 'next/navigation'`
|
||||
- `import { redirect } from 'next/navigation'`
|
||||
- `import Link from 'next/link'`
|
||||
|
||||
### 4. **Component Classification (Suggestions)**
|
||||
```json
|
||||
{
|
||||
"files": ["components/**/*", "ui/**/*"],
|
||||
"rules": {
|
||||
"gridpilot-rules/component-classification": "warn"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Suggests**:
|
||||
- Move pure components to ui/
|
||||
- Move stateful elements to components/
|
||||
|
||||
---
|
||||
|
||||
## Dependency Flow
|
||||
|
||||
```
|
||||
app/ (pages)
|
||||
↓ imports from
|
||||
components/ (stateful)
|
||||
↓ imports from
|
||||
ui/ (pure)
|
||||
↓ imports from
|
||||
hooks/ (logic)
|
||||
```
|
||||
|
||||
**Never**:
|
||||
- app/ → hooks/ (pages don't need hooks directly)
|
||||
- ui/ → components/ (ui must stay pure)
|
||||
- ui/ → next/navigation (ui must be framework-agnostic)
|
||||
- components/ → next/navigation (navigation should be passed from pages)
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### ✅ Correct Architecture
|
||||
|
||||
```typescript
|
||||
// app/dashboard/page.tsx
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<DashboardHeader />
|
||||
<DashboardStats />
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
// components/DashboardHeader.tsx
|
||||
export function DashboardHeader() {
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
return (
|
||||
<header>
|
||||
<SearchInput value={search} onChange={setSearch} />
|
||||
<Button onClick={handleSearch}>Search</Button>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
// ui/SearchInput.tsx
|
||||
export function SearchInput({ value, onChange }) {
|
||||
return (
|
||||
<input
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
className="border p-2"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ui/Button.tsx
|
||||
export function Button({ children, onClick }) {
|
||||
return (
|
||||
<button onClick={onClick} className="bg-blue-500 text-white">
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Wrong Architecture
|
||||
|
||||
```typescript
|
||||
// app/dashboard/page.tsx
|
||||
export default function DashboardPage() {
|
||||
const [search, setSearch] = useState(''); // ❌ State in page
|
||||
|
||||
return (
|
||||
<div className="p-6"> {/* ❌ Raw HTML */}
|
||||
<input
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)} // ❌ State logic
|
||||
/>
|
||||
<button onClick={() => router.push('/search')}> {/* ❌ Router in page */}
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Reusability**: UI elements can be used anywhere
|
||||
2. **Testability**: Pure functions are easy to test
|
||||
3. **Maintainability**: Clear separation of concerns
|
||||
4. **Performance**: No unnecessary re-renders
|
||||
5. **Type Safety**: Each layer has clear contracts
|
||||
|
||||
---
|
||||
|
||||
## Migration Guide
|
||||
|
||||
If you have existing code that violates these rules:
|
||||
|
||||
1. **Extract UI elements** from app/ to ui/
|
||||
2. **Move stateful logic** from ui/ to components/
|
||||
3. **Remove Next.js imports** from components/ui (pass callbacks from pages)
|
||||
4. **Use hooks/** for shared logic
|
||||
|
||||
**Example migration**:
|
||||
```typescript
|
||||
// Before
|
||||
// app/page.tsx
|
||||
export default function Page() {
|
||||
return <div className="bg-white p-6">...</div>;
|
||||
}
|
||||
|
||||
// After
|
||||
// app/page.tsx
|
||||
export default function Page() {
|
||||
return <HomePage />;
|
||||
}
|
||||
|
||||
// components/HomePage.tsx
|
||||
export function HomePage() {
|
||||
return (
|
||||
<PageContainer>
|
||||
<HomeContent />
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// ui/PageContainer.tsx
|
||||
export function PageContainer({ children }) {
|
||||
return <div className="bg-white p-6">{children}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Layer | State | Next.js | HTML | Purpose |
|
||||
|-------|-------|---------|------|---------|
|
||||
| **app/** | ❌ No | ✅ Yes | ❌ No | Pages & layouts |
|
||||
| **components/** | ✅ Yes | ❌ No | ❌ No | App components |
|
||||
| **ui/** | ❌ No | ❌ No | ✅ Yes | Pure elements |
|
||||
| **hooks/** | ✅ Yes | ❌ No | ❌ No | Shared logic |
|
||||
|
||||
**Golden Rule**: Each layer should only depend on layers below it.
|
||||
Reference in New Issue
Block a user