Initial commit: Kottgard production website code
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
# Kött Gård — Clean Architecture Guide
|
||||
|
||||
This document explains **what we changed**, **why**, and **how to work in the new structure**.
|
||||
|
||||
---
|
||||
|
||||
## What is Clean Architecture?
|
||||
|
||||
Clean Architecture splits code into **layers**. Inner layers hold business rules. Outer layers hold frameworks (React, Next.js, Zustand, localStorage).
|
||||
|
||||
**Golden rule:** Dependencies point **inward only**.
|
||||
|
||||
```
|
||||
Presentation → Application → Domain
|
||||
Infrastructure → Application → Domain
|
||||
|
||||
Domain imports NOTHING from outside itself.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Before vs After
|
||||
|
||||
### Before (mixed responsibilities)
|
||||
|
||||
```
|
||||
page.tsx → filter products, calculate delivery fee, create orders
|
||||
store/cart.ts → persistence + merge logic + totals
|
||||
lib/products.ts → data + queries in one file
|
||||
```
|
||||
|
||||
Problems:
|
||||
- Delivery fee `total > 500 ? 0 : 49` was copy-pasted in cart **and** checkout
|
||||
- Shop filtering logic lived inside the React page
|
||||
- Hard to swap localStorage for a real API later
|
||||
|
||||
### After (separated layers)
|
||||
|
||||
| Layer | Folder | Job |
|
||||
|-------|--------|-----|
|
||||
| **Domain** | `src/domain/` | Business rules — cart math, delivery fee, order IDs |
|
||||
| **Application** | `src/application/` | Use cases + ports (interfaces) |
|
||||
| **Infrastructure** | `src/infrastructure/` | Zustand, product data file, i18n adapter |
|
||||
| **Presentation** | `src/app/`, `src/components/`, `src/presentation/hooks/` | UI only — calls use cases |
|
||||
|
||||
---
|
||||
|
||||
## Layer diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Presentation
|
||||
P1[app/pages]
|
||||
P2[components]
|
||||
P3[presentation/hooks]
|
||||
end
|
||||
|
||||
subgraph Application
|
||||
A1[use-cases]
|
||||
A2[ports interfaces]
|
||||
A3[container.ts]
|
||||
end
|
||||
|
||||
subgraph Domain
|
||||
D1[entities]
|
||||
D2[domain services]
|
||||
end
|
||||
|
||||
subgraph Infrastructure
|
||||
I1[Zustand stores]
|
||||
I2[InMemoryProductRepository]
|
||||
I3[products.data.ts]
|
||||
end
|
||||
|
||||
P1 --> P3
|
||||
P3 --> A3
|
||||
A3 --> A1
|
||||
A1 --> A2
|
||||
A1 --> D2
|
||||
A2 -.implemented by.- I1
|
||||
A2 -.implemented by.- I2
|
||||
I2 --> I3
|
||||
D2 --> D1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step-by-step: what we did
|
||||
|
||||
### Step 1 — Domain entities (`src/domain/entities/`)
|
||||
|
||||
**What:** Moved `Product`, `CartItem`, `Order`, `User`, etc. from `types/` into the domain.
|
||||
|
||||
**Why:** Entities are the core vocabulary of the business. They must not depend on React or Next.js.
|
||||
|
||||
**Example:** `Product` describes a meat item (price, category, slug) — not how it is rendered.
|
||||
|
||||
```
|
||||
src/domain/entities/index.ts
|
||||
```
|
||||
|
||||
`src/types/index.ts` now **re-exports** domain entities so old imports still work.
|
||||
|
||||
---
|
||||
|
||||
### Step 2 — Domain services (`src/domain/services/`)
|
||||
|
||||
**What:** Pure functions / classes for business rules.
|
||||
|
||||
| Service | Rule extracted from |
|
||||
|---------|---------------------|
|
||||
| `CartDomainService` | `store/cart.ts` — add item, merge duplicates, totals |
|
||||
| `DeliveryFeeService` | `cart/page.tsx` + `checkout/page.tsx` — 49 kr fee, free over 500 kr |
|
||||
| `CustomizationDomainService` | `lib/customization.ts` — cut styles, cart line keys |
|
||||
| `OrderDomainService` | `checkout/page.tsx` — order ID format, order object shape |
|
||||
| `AuthDomainService` | `store/auth.ts` — demo password check, user creation |
|
||||
| `CatalogDomainService` | `shop/page.tsx` — filter by category, search, sort |
|
||||
|
||||
**Why:** One place per rule. Change delivery threshold once → cart and checkout both update.
|
||||
|
||||
**Example — delivery fee (single source of truth):**
|
||||
|
||||
```typescript
|
||||
// src/domain/constants/commerce.ts
|
||||
export const FREE_DELIVERY_THRESHOLD_SEK = 500;
|
||||
export const STANDARD_DELIVERY_FEE_SEK = 49;
|
||||
|
||||
// src/domain/services/DeliveryFeeService.ts
|
||||
static calculateDeliveryFee(subtotal: number): number {
|
||||
return subtotal > FREE_DELIVERY_THRESHOLD_SEK ? 0 : STANDARD_DELIVERY_FEE_SEK;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3 — Application ports (`src/application/ports/`)
|
||||
|
||||
**What:** TypeScript **interfaces** describing what the app needs — not how it is stored.
|
||||
|
||||
| Port | Contract |
|
||||
|------|----------|
|
||||
| `IProductRepository` | `findAll()`, `findBySlug()`, `findFeatured()` |
|
||||
| `ICartRepository` | `getItems()`, `addItem()`, `clearCart()` |
|
||||
| `IAuthRepository` | `login()`, `register()`, `addOrder()` |
|
||||
| `IWishlistRepository` | `toggleItem()`, `isInWishlist()` |
|
||||
| `ITranslationService` | `translate(key)` |
|
||||
|
||||
**Why:** Today products live in a static array. Tomorrow they might come from Shopify or a database. **Only the infrastructure adapter changes** — use cases stay the same.
|
||||
|
||||
---
|
||||
|
||||
### Step 4 — Use cases (`src/application/use-cases/`)
|
||||
|
||||
**What:** One class per user action. Orchestrates domain + ports.
|
||||
|
||||
| Use case | Replaces logic in |
|
||||
|----------|-------------------|
|
||||
| `FilterProductsUseCase` | Shop page filtering/sorting |
|
||||
| `GetProductBySlugUseCase` | `getProductBySlug()` calls |
|
||||
| `LocalizeProductUseCase` | `lib/product-i18n.ts` |
|
||||
| `AddToCartUseCase` | Cart add button |
|
||||
| `GetCartSummaryUseCase` | Cart totals + delivery |
|
||||
| `PlaceOrderUseCase` | Checkout submit handler |
|
||||
| `LoginUseCase` / `RegisterUseCase` | Login page |
|
||||
|
||||
**Example — checkout flow:**
|
||||
|
||||
```
|
||||
User clicks Pay
|
||||
→ PlaceOrderUseCase.execute()
|
||||
→ OrderDomainService.createOrder() [domain]
|
||||
→ authRepository.addOrder() [port]
|
||||
→ cartRepository.clearCart() [port]
|
||||
```
|
||||
|
||||
File: `src/application/use-cases/checkout/PlaceOrder.ts`
|
||||
|
||||
---
|
||||
|
||||
### Step 5 — Infrastructure (`src/infrastructure/`)
|
||||
|
||||
**What:** Concrete implementations of ports + framework code.
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `data/products.data.ts` | Static 16-product catalog |
|
||||
| `repositories/InMemoryProductRepository.ts` | Implements `IProductRepository` |
|
||||
| `persistence/zustand/cartStore.ts` | Zustand + `ZustandCartRepository` |
|
||||
| `persistence/zustand/authStore.ts` | Auth persistence + demo user |
|
||||
| `persistence/zustand/wishlistStore.ts` | Wishlist persistence |
|
||||
| `i18n/I18nTranslationService.ts` | Wraps existing `i18n/` dictionaries |
|
||||
|
||||
**Why Zustand stays:** It is an **infrastructure detail** (browser storage). The application layer only sees `ICartRepository`.
|
||||
|
||||
---
|
||||
|
||||
### Step 6 — Composition root (`src/application/container.ts`)
|
||||
|
||||
**What:** One file that wires everything together.
|
||||
|
||||
```typescript
|
||||
export const container = new ApplicationContainer();
|
||||
// container.addToCart.execute(...)
|
||||
// container.getCartSummary.execute()
|
||||
// container.placeOrder.execute(...)
|
||||
```
|
||||
|
||||
**Why:** Pages and hooks do not `new InMemoryProductRepository()` themselves. That would couple UI to infrastructure. The container is the **only** place that knows concrete classes.
|
||||
|
||||
---
|
||||
|
||||
### Step 7 — Presentation hooks (`src/presentation/hooks/`)
|
||||
|
||||
**What:** React-friendly API on top of the container.
|
||||
|
||||
| Hook | Use in |
|
||||
|------|--------|
|
||||
| `useCart()` | Cart page, Header, product page |
|
||||
| `useAuth()` | Login, account, checkout |
|
||||
| `useWishlist()` | Product card, wishlist page |
|
||||
| `useCheckout()` | Checkout submit |
|
||||
| `useCatalogFilter()` | Shop page |
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
// Old (page knew about Zustand internals + business math)
|
||||
const { getTotal } = useCartStore();
|
||||
const deliveryFee = total > 500 ? 0 : 49;
|
||||
|
||||
// New (page uses use case result)
|
||||
const { summary } = useCart();
|
||||
const { subtotal, deliveryFee, grandTotal } = summary;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 8 — Backward-compatible facades
|
||||
|
||||
Old paths still work so nothing breaks silently:
|
||||
|
||||
| Old import | Now points to |
|
||||
|------------|---------------|
|
||||
| `@/types` | `@/domain/entities` |
|
||||
| `@/lib/customization` | `CustomizationDomainService` |
|
||||
| `@/lib/products` | `container.productRepository` |
|
||||
| `@/store/cart` | infrastructure + `useCart` hook |
|
||||
|
||||
---
|
||||
|
||||
## New folder map
|
||||
|
||||
```
|
||||
src/
|
||||
├── domain/
|
||||
│ ├── entities/ # Product, Order, CartItem…
|
||||
│ ├── constants/ # FREE_DELIVERY_THRESHOLD_SEK
|
||||
│ └── services/ # CartDomainService, DeliveryFeeService…
|
||||
│
|
||||
├── application/
|
||||
│ ├── ports/ # IProductRepository, ICartRepository…
|
||||
│ ├── use-cases/ # AddToCart, PlaceOrder, FilterProducts…
|
||||
│ ├── dtos/ # LocalizedProduct, CartSummary
|
||||
│ └── container.ts # ★ wires everything
|
||||
│
|
||||
├── infrastructure/
|
||||
│ ├── data/ # products.data.ts, demo-orders.data.ts
|
||||
│ ├── repositories/ # InMemoryProductRepository
|
||||
│ ├── persistence/zustand/ # Stores + repository adapters
|
||||
│ └── i18n/ # I18nTranslationService
|
||||
│
|
||||
├── presentation/
|
||||
│ └── hooks/ # useCart, useAuth, useCatalog…
|
||||
│
|
||||
├── app/ # Thin pages (UI + hooks)
|
||||
├── components/ # Visual components
|
||||
├── hooks/useTranslation.ts # Still used for t('key')
|
||||
├── lib/ # Facades (backward compat)
|
||||
├── store/ # Re-exports (backward compat)
|
||||
└── types/ # Re-exports (backward compat)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How to add a feature (cheat sheet)
|
||||
|
||||
### Change delivery fee rule
|
||||
1. Edit `src/domain/constants/commerce.ts`
|
||||
2. Done — `GetCartSummaryUseCase` and `PlaceOrderUseCase` pick it up automatically
|
||||
|
||||
### Add a new product
|
||||
1. Add entry in `src/infrastructure/data/products.data.ts`
|
||||
2. Add translations in `src/i18n/locales/*.ts`
|
||||
|
||||
### Add a new page action (e.g. "apply coupon")
|
||||
1. Add rule in `src/domain/services/` if it is business logic
|
||||
2. Add port method if it needs storage
|
||||
3. Create `src/application/use-cases/cart/ApplyCoupon.ts`
|
||||
4. Register in `container.ts`
|
||||
5. Expose via `useCart()` or new hook
|
||||
6. Call from page/component
|
||||
|
||||
### Replace static products with an API
|
||||
1. Create `src/infrastructure/repositories/ApiProductRepository.ts` implementing `IProductRepository`
|
||||
2. In `container.ts`, swap `InMemoryProductRepository` → `ApiProductRepository`
|
||||
3. **No changes** to shop page, use cases, or domain
|
||||
|
||||
---
|
||||
|
||||
## Dependency rules (memorize this)
|
||||
|
||||
| Layer | Can import |
|
||||
|-------|------------|
|
||||
| Domain | Only domain |
|
||||
| Application | Domain + its own ports/DTOs |
|
||||
| Infrastructure | Application ports + Domain |
|
||||
| Presentation | Application container + hooks + React |
|
||||
|
||||
| Layer | Cannot import |
|
||||
|-------|---------------|
|
||||
| Domain | React, Next, Zustand, `app/`, `components/` |
|
||||
| Application | Zustand, `page.tsx`, JSX |
|
||||
| Use cases | Concrete repositories (only interfaces) |
|
||||
|
||||
---
|
||||
|
||||
## Request flow example: Add to cart
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Page as product/page.tsx
|
||||
participant Hook as useCart()
|
||||
participant UC as AddToCartUseCase
|
||||
participant Repo as ZustandCartRepository
|
||||
participant Domain as CartDomainService
|
||||
participant Store as Zustand persist
|
||||
|
||||
Page->>Hook: addItem(product, customization, label)
|
||||
Hook->>UC: execute(...)
|
||||
UC->>Repo: addItem(...)
|
||||
Repo->>Domain: addItem(items, product, ...)
|
||||
Domain-->>Repo: newItems[]
|
||||
Repo->>Store: setItems(newItems)
|
||||
Store-->>Page: React re-render
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verify the project
|
||||
|
||||
```bash
|
||||
cd /Users/apple/Desktop/code/Kottgard
|
||||
npm run build # must pass
|
||||
npm run dev # test at http://localhost:3000
|
||||
```
|
||||
|
||||
Demo login: `demo@kottgard.se` / `demo123`
|
||||
|
||||
---
|
||||
|
||||
## Further reading
|
||||
|
||||
- Uncle Bob — Clean Architecture (concentric circles diagram)
|
||||
- This project's annotated code: `docs/annotated/src/`
|
||||
- Beginner folder map: `~/Desktop/Kottgard-Project-Learn/`
|
||||
Reference in New Issue
Block a user