chore: initial monorepo scaffold

Setup pnpm workspaces with apps/{web,api} placeholders and shared
packages: tsconfig, eslint-config, shared, ui, db. Includes Prettier,
ESLint, Husky pre-commit, lint-staged, EditorConfig, VSCode settings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Opus 4.7 committed 2026-04-29 12:10:08 +02:00
commit 87ca5af765
40 files changed
+700

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
+14
View File
@@ -0,0 +1,14 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
extends: ['@ketopath/eslint-config'],
ignorePatterns: [
'node_modules/',
'dist/',
'build/',
'.next/',
'out/',
'coverage/',
'pnpm-lock.yaml',
],
};
+11
View File
@@ -0,0 +1,11 @@
* text=auto eol=lf
*.docx binary
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.pdf binary
pnpm-lock.yaml linguist-generated=true -diff
View File
Whitespace-only changes.
+41
View File
@@ -0,0 +1,41 @@
# Dependencies
node_modules/
.pnpm-store/
# Build output
dist/
build/
.next/
out/
.turbo/
# Test / coverage
coverage/
.nyc_output/
# Env files (NEVER commit)
.env
.env.*
!.env.example
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# OS
.DS_Store
Thumbs.db
# Editor
.idea/
*.swp
*.swo
# TypeScript
*.tsbuildinfo
# Misc
.cache/
+1
View File
@@ -0,0 +1 @@
pnpm lint-staged
+3
View File
@@ -0,0 +1,3 @@
engine-strict=true
auto-install-peers=true
node-linker=isolated
+1
View File
@@ -0,0 +1 @@
20.11.1
+14
View File
@@ -0,0 +1,14 @@
node_modules/
.pnpm-store/
dist/
build/
.next/
out/
.turbo/
coverage/
pnpm-lock.yaml
*.docx
*.pdf
# User-authored project doc — preserve exact formatting
CLAUDE.md
+10
View File
@@ -0,0 +1,10 @@
{
"singleQuote": true,
"semi": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"arrowParens": "always",
"endOfLine": "lf",
"plugins": ["prettier-plugin-tailwindcss"]
}
+11
View File
@@ -0,0 +1,11 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.workingDirectories": [{ "pattern": "apps/*" }, { "pattern": "packages/*" }],
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"files.eol": "\n"
}
+208
View File
@@ -0,0 +1,208 @@
# KetoPath
> Web app responsive multi-utente per la gestione personalizzata di chetogenica e digiuno intermittente, con piani alimentari adattivi, tracking peso/progressi, gestione del digiuno e lista della spesa automatica.
Questo file viene letto automaticamente da Claude Code a ogni sessione. Tienilo aggiornato con le decisioni architetturali e di prodotto.
---
## Vision
Trasformare la chetogenica e il digiuno intermittente da diete temporanee a stili di vita strutturati, accompagnando ogni utente dalla prima settimana di adattamento al mantenimento per la vita.
Il documento di prodotto completo è in `docs/PRD_KetoPath.docx`. Quando devi prendere decisioni di prodotto non banali, leggilo prima di proporre soluzioni.
---
## Tech stack
**Frontend**
- Next.js 14 (App Router) + TypeScript strict mode
- Tailwind CSS + shadcn/ui per i componenti
- TanStack Query per data fetching client-side
- Zustand per stato globale leggero
- Zod per validazione form e tipi runtime
**Backend**
- Node.js + Fastify + TypeScript
- Prisma come ORM
- Auth gestita via Clerk (no roll-your-own)
**Database & infrastruttura**
- PostgreSQL 15+ come DB principale
- Redis per cache, sessioni e queue di job
- Cloudflare R2 per storage foto progress
- SendGrid per email transazionali
- Firebase Cloud Messaging per push web
**Hosting & DevOps**
- Vercel per il frontend Next.js
- Render o Fly.io per il backend Fastify
- GitHub Actions per CI/CD
- PostHog (self-hosted) per analytics
- Sentry per error tracking
---
## Repository structure
```
ketopath/
├── apps/
│ ├── web/ # Next.js frontend (App Router)
│ └── api/ # Fastify backend
├── packages/
│ ├── db/ # Prisma schema, client, migrations
│ ├── shared/ # Tipi TS, utility e logica di dominio condivisa
│ └── ui/ # Componenti UI riusabili (estensione shadcn/ui)
├── docs/
│ ├── PRD_KetoPath.docx
│ └── decisions/ # ADR (Architecture Decision Records)
├── .github/workflows/
└── CLAUDE.md # questo file
```
Usa un monorepo con pnpm workspaces o Turborepo. Tutto in TypeScript.
---
## Convenzioni di codice
- **TypeScript strict mode** sempre. Mai `any` senza commento `// eslint-disable-next-line` motivato.
- **File**: `kebab-case.tsx` per i moduli, `PascalCase.tsx` per i componenti React esposti.
- **Componenti React**: function components + hooks, no class components.
- **Naming**:
- Variabili e funzioni: `camelCase`
- Componenti, tipi e interfacce: `PascalCase`
- Costanti globali: `SCREAMING_SNAKE_CASE`
- **Commit message**: Conventional Commits (`feat:`, `fix:`, `chore:`, `docs:`, ecc.).
- **Linting**: ESLint + Prettier, formattazione automatica al pre-commit con husky + lint-staged.
- **Test**: Vitest per unit test, Playwright per E2E. Coverage minima 70% sui moduli di dominio (calcoli BMR, macros, fasi).
---
## Pattern di codice da preferire
- **Server Components** di default in Next.js; `"use client"` solo dove serve interattività.
- **Server Actions** per mutazioni semplici di form, API routes per logica più complessa.
- **Suspense + Error Boundary** per ogni feature.
- **Form**: React Hook Form + Zod resolver. Mai validazione manuale.
- **Date**: usa `date-fns`. Mai `Date.parse` o stringhe ad-hoc.
- **DB queries**: Prisma client. Niente raw SQL se non strettamente necessario, e in quel caso commenta perché.
- **Optimistic updates** per le azioni utente frequenti (segnare un pasto come consumato, spuntare un articolo della lista spesa).
---
## Conoscenza di dominio
Il dominio dell'app ha alcuni concetti chiave che devi conoscere prima di scrivere codice:
- **3 Fasi del percorso utente**:
- `INTENSIVE` (giorni 1-30/45) — deficit calorico aggressivo, IF stretto
- `TRANSITION` (giorni 31-90) — reverse dieting, calorie in salita
- `MAINTENANCE` (per sempre) — 14:10, regola 80/20, soglia di allarme
- **Calcolo calorie** con formula Mifflin-St Jeor:
- Uomini: `BMR = 10 × peso(kg) + 6.25 × altezza(cm) − 5 × età + 5`
- Donne: `BMR = 10 × peso(kg) + 6.25 × altezza(cm) − 5 × età − 161`
- TDEE = BMR × fattore attività (1.2 / 1.375 / 1.55 / 1.725)
- **Macros target per fase**:
- Carboidrati netti: 5-10% in Phase 1, sale in Phase 2-3
- Proteine: 1.5-1.8 g/kg di peso ideale
- Grassi: il resto
- **Protocolli di digiuno**: enum `FASTING_PROTOCOL = 14_10 | 16_8 | 18_6 | 20_4 | ESE_24 | FIVE_TWO`
- **Esclusioni alimentari**: ogni `Ingredient` ha `exclusion_groups` (es. `lactose`, `fish`, `nuts`); ogni `Recipe` eredita le esclusioni dai suoi ingredienti
Quando implementi una nuova funzionalità, mappa sempre prima a quale fase appartiene e quali macros/vincoli rispetta.
---
## Comandi comuni
```bash
# Sviluppo
pnpm dev # Avvia frontend + backend in parallelo
pnpm dev:web # Solo Next.js
pnpm dev:api # Solo Fastify
# Build & deploy
pnpm build # Build di tutti i package
pnpm start # Avvia in production mode
# Database
pnpm db:migrate # prisma migrate dev
pnpm db:studio # Apre Prisma Studio
pnpm db:seed # Popola DB con dati di test
# Quality
pnpm lint # ESLint
pnpm format # Prettier
pnpm typecheck # tsc --noEmit
pnpm test # Vitest
pnpm test:e2e # Playwright
```
---
## Vincoli non negoziabili
- **Lingua UI di default: italiano**. Tutti i testi user-facing in italiano. Predisporre i18n con `next-intl` per l'eventuale internazionalizzazione futura.
- **Mobile-first responsive**: ogni pagina deve essere progettata e testata prima a 375px di larghezza.
- **GDPR by design**: i dati di salute (peso, misure, foto, sintomi) rientrano nell'art. 9 GDPR. Crittografia at-rest sul DB, niente log dei dati sanitari, consenso esplicito.
- **Disclaimer medico** sempre visibile in onboarding, nelle pagine di calcolo calorico e nelle pagine di tracking. Testo da concordare con un medico advisor.
- **Privacy first**: niente tracking di terze parti senza consenso, no Google Analytics di default. Se proprio servono analytics, usa PostHog self-hosted con cookie-less mode.
- **Accessibilità WCAG 2.1 AA**: tutto il contenuto navigabile da tastiera, contrasto colori sufficiente, alt text sulle immagini, semantica HTML corretta.
---
## Cosa NON fare mai
- Salvare password in chiaro (Clerk gestisce auth, non implementarla a mano).
- Loggare dati di salute fuori dal DB cifrato.
- Usare `eval()` o `new Function()` con input utente.
- Disabilitare feature di sicurezza (CSP, CORS, rate limiting) per "comodità di sviluppo".
- Fare claim medici sui prodotti o suggerire farmaci/integratori specifici.
- Esportare dati utente in chiaro senza autenticazione e log dell'export.
- Fare commit di file `.env`, chiavi API, dump di database.
- Usare librerie deprecate o senza manutenzione attiva (controlla l'ultimo commit prima di aggiungere una dipendenza).
---
## Workflow consigliato per Claude Code
Quando ti chiedo una nuova funzionalità, segui questo flusso:
1. **Domanda di chiarimento** se il task non è completamente specificato.
2. **Lettura preventiva**: leggi i file pertinenti prima di proporre modifiche, non scrivere codice "alla cieca".
3. **Plan mode** per task non banali: presentami un piano in 5-10 punti prima di toccare il codice.
4. **Implementazione progressiva**: feature piccole, commit frequenti, ogni commit verde (lint + test passano).
5. **Diff esplicito**: mostrami sempre i diff prima di applicare modifiche a file critici (`prisma/schema.prisma`, `package.json`, file di config).
6. **Test**: per ogni nuova logica di dominio (calcoli macros, fasi, vincoli) scrivi anche unit test.
7. **Documentazione**: aggiorna il README e questo CLAUDE.md quando introduci pattern nuovi o cambi decisioni architetturali.
---
## Decisioni architetturali aperte (da prendere insieme)
Queste decisioni non sono ancora finalizzate. Se le tocchi, aprire un ADR in `docs/decisions/`.
- [ ] Stripe vs Lemon Squeezy per il payment provider
- [ ] PostHog self-hosted vs Mixpanel per analytics di prodotto
- [ ] Render vs Fly.io vs AWS ECS per l'hosting backend
- [ ] Strategia di seeding del database ricette (manuale vs scraping autorizzato vs LLM-assisted)
- [ ] Localizzazione: i18n da subito o solo italiano per MVP?
- [ ] App mobile nativa post-MVP: React Native o Expo?
---
## Riferimenti
- PRD completo: `docs/PRD_KetoPath.docx`
- Documentazione Next.js: https://nextjs.org/docs
- Documentazione Prisma: https://www.prisma.io/docs
- Documentazione Clerk: https://clerk.com/docs
- shadcn/ui: https://ui.shadcn.com
- GDPR e dati sanitari: https://www.garanteprivacy.it/
---
*Ultimo aggiornamento: aprile 2026. Modifica questo file ogni volta che cambia una decisione architetturale o un pattern di codice.*
+46
View File
@@ -0,0 +1,46 @@
# KetoPath
Web app responsive multi-utente per la gestione personalizzata di chetogenica e digiuno intermittente.
> Vedi `CLAUDE.md` per le convenzioni di progetto e `docs/PRD_KetoPath.docx` per il PRD completo.
## Prerequisiti
- Node.js `>=20.11` (vedi `.nvmrc`)
- pnpm `>=9` (`corepack enable && corepack prepare pnpm@latest --activate`)
## Getting started
```bash
pnpm install
pnpm typecheck
pnpm lint
pnpm format:check
```
## Struttura del monorepo
```
apps/
web/ # Next.js 14 frontend (da scaffoldare)
api/ # Fastify backend (da scaffoldare)
packages/
shared/ # Tipi e utility di dominio
ui/ # Componenti UI riusabili
db/ # Prisma client (da scaffoldare)
tsconfig/ # tsconfig presets condivisi
eslint-config/ # ESLint config condivisa
docs/
PRD_KetoPath.docx
```
## Script disponibili
- `pnpm dev` — avvia tutte le app in parallelo
- `pnpm dev:web` / `pnpm dev:api` — avvia singola app
- `pnpm build` — build di produzione
- `pnpm lint` / `pnpm lint:fix` — ESLint
- `pnpm format` / `pnpm format:check` — Prettier
- `pnpm typecheck` — `tsc -b` su tutto il monorepo
- `pnpm test` — Vitest (unit test)
- `pnpm test:e2e` — Playwright (E2E)
View File
Whitespace-only changes.
View File
Whitespace-only changes.
Binary file not shown.
+48
View File
@@ -0,0 +1,48 @@
{
"name": "ketopath",
"version": "0.0.0",
"private": true,
"description": "KetoPath monorepo — web app per la gestione di chetogenica e digiuno intermittente",
"packageManager": "pnpm@9.12.0",
"engines": {
"node": ">=20.11",
"pnpm": ">=9"
},
"scripts": {
"dev": "pnpm --parallel --filter \"./apps/*\" dev",
"dev:web": "pnpm --filter @ketopath/web dev",
"dev:api": "pnpm --filter @ketopath/api dev",
"build": "pnpm --filter \"./apps/*\" build",
"start": "pnpm --filter \"./apps/*\" start",
"lint": "eslint . --max-warnings=0",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"typecheck": "tsc -b",
"test": "pnpm --filter \"./packages/*\" --filter \"./apps/*\" -r --if-present test",
"test:e2e": "pnpm --filter @ketopath/web test:e2e",
"prepare": "husky"
},
"devDependencies": {
"@types/node": "^20.12.7",
"@typescript-eslint/eslint-plugin": "^7.7.0",
"@typescript-eslint/parser": "^7.7.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.14",
"typescript": "5.4.5"
},
"lint-staged": {
"*.{ts,tsx,js,jsx,cjs,mjs}": [
"eslint --fix --max-warnings=0",
"prettier --write"
],
"*.{json,md,yml,yaml,css}": [
"prettier --write"
]
}
}
+5
View File
@@ -0,0 +1,5 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
extends: ['@ketopath/eslint-config/node.cjs'],
};
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@ketopath/db",
"version": "0.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "eslint src --max-warnings=0"
},
"devDependencies": {
"@ketopath/eslint-config": "workspace:*",
"@ketopath/tsconfig": "workspace:*"
}
}
+1
View File
@@ -0,0 +1 @@
export {};
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "@ketopath/tsconfig/node.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"composite": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+65
View File
@@ -0,0 +1,65 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: false,
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
plugins: ['@typescript-eslint', 'import'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'prettier',
],
settings: {
'import/resolver': {
typescript: {
alwaysTryTypes: true,
},
node: true,
},
},
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
],
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
alphabetize: { order: 'asc', caseInsensitive: true },
},
],
'import/no-default-export': 'off',
},
overrides: [
{
files: ['*.cjs', '*.config.js', '*.config.mjs'],
env: { node: true },
rules: {
'@typescript-eslint/no-var-requires': 'off',
},
},
],
ignorePatterns: [
'node_modules/',
'dist/',
'build/',
'.next/',
'out/',
'coverage/',
'*.config.js',
'*.config.cjs',
'*.config.mjs',
],
};
+8
View File
@@ -0,0 +1,8 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [require.resolve('./index.cjs'), 'next/core-web-vitals'],
rules: {
'react/no-unescaped-entities': 'off',
'@next/next/no-html-link-for-pages': 'off',
},
};
+11
View File
@@ -0,0 +1,11 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [require.resolve('./index.cjs')],
env: {
node: true,
es2022: true,
},
rules: {
'no-console': ['warn', { allow: ['warn', 'error', 'info'] }],
},
};
+14
View File
@@ -0,0 +1,14 @@
{
"name": "@ketopath/eslint-config",
"version": "0.0.0",
"private": true,
"main": "index.cjs",
"files": [
"index.cjs",
"nextjs.cjs",
"node.cjs"
],
"peerDependencies": {
"eslint": "^8.57.0"
}
}
+5
View File
@@ -0,0 +1,5 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
extends: ['@ketopath/eslint-config'],
};
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@ketopath/shared",
"version": "0.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "eslint src --max-warnings=0"
},
"devDependencies": {
"@ketopath/eslint-config": "workspace:*",
"@ketopath/tsconfig": "workspace:*"
}
}
+1
View File
@@ -0,0 +1 @@
export {};
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "@ketopath/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"composite": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+30
View File
@@ -0,0 +1,30 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Base",
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"moduleResolution": "Bundler",
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"incremental": true
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Next.js",
"extends": "./base.json",
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"jsx": "preserve",
"allowJs": true,
"noEmit": true,
"plugins": [{ "name": "next" }]
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Node.js 20",
"extends": "./base.json",
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"types": ["node"]
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "@ketopath/tsconfig",
"version": "0.0.0",
"private": true,
"files": [
"base.json",
"nextjs.json",
"node.json"
]
}
+5
View File
@@ -0,0 +1,5 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
extends: ['@ketopath/eslint-config'],
};
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@ketopath/ui",
"version": "0.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "eslint src --max-warnings=0"
},
"devDependencies": {
"@ketopath/eslint-config": "workspace:*",
"@ketopath/tsconfig": "workspace:*"
}
}
+1
View File
@@ -0,0 +1 @@
export {};
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "@ketopath/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"composite": true,
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"jsx": "preserve"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
BIN
View File
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
packages:
- 'apps/*'
- 'packages/*'
+8
View File
@@ -0,0 +1,8 @@
{
"files": [],
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/ui" },
{ "path": "./packages/db" }
]
}