Initial commit: Kottgard production website code

This commit is contained in:
2026-06-19 15:46:23 +00:00
commit e0273c46d7
196 changed files with 29799 additions and 0 deletions
+365
View File
@@ -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/`