feat: initialiser le projet CVP (Next.js 14 + FastAPI + PostgreSQL)

- Frontend : Next.js 14 App Router, TypeScript strict, Tailwind 3, shadcn/ui, next-intl (fr/en)
- Backend : FastAPI, SQLAlchemy 2 async, Alembic, Pydantic 2, Python 3.11
- Infrastructure : Docker Compose (PostgreSQL 16 + Ollama)
- Tooling : ESLint + Prettier (frontend), Ruff (backend), pytest
- Structure complète des dossiers avec pages et routers placeholder
This commit is contained in:
2026-04-13 15:28:01 +02:00
parent 513251f004
commit f1d5882596
68 changed files with 8606 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
# PostgreSQL
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=cvp
POSTGRES_USER=cvp
POSTGRES_PASSWORD=cvp_dev_password
# France Travail API
FRANCE_TRAVAIL_CLIENT_ID=
FRANCE_TRAVAIL_CLIENT_SECRET=
# Adzuna API
ADZUNA_APP_ID=
ADZUNA_APP_KEY=
# Ollama
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=phi3:mini
# Backend
BACKEND_HOST=0.0.0.0
BACKEND_PORT=8000
# Frontend
NEXT_PUBLIC_API_URL=http://localhost:8000
+42
View File
@@ -0,0 +1,42 @@
# Dependencies
node_modules/
__pycache__/
*.pyc
.venv/
venv/
# Environment
.env
.env.local
.env.*.local
# Build
.next/
out/
dist/
build/
*.egg-info/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Testing
coverage/
htmlcov/
.pytest_cache/
.coverage
# Database
*.db
*.sqlite3
# Generated
*.pdf
*.docx
+114
View File
@@ -0,0 +1,114 @@
# CVP — CV Personnalisé
Application web locale de recherche d'emploi intelligente. Agrège des offres (France Travail, Adzuna), analyse la compatibilité profil/offre via un LLM local (Ollama), et génère des CV adaptés.
## Stack
- **Frontend** : Next.js 14 (App Router), React 18, TypeScript strict, Tailwind CSS 3, shadcn/ui (new-york), next-intl (fr/en)
- **Backend** : Python 3.11, FastAPI, SQLAlchemy 2 (async), Alembic, Pydantic 2
- **Base de données** : PostgreSQL 16 (via Docker Compose)
- **LLM local** : Ollama + Phi-3 mini (optionnel — l'app fonctionne sans)
## Commandes
### Frontend (`cd frontend`)
```bash
npm run dev # Serveur de développement (port 3000)
npm run build # Build production
npm run lint # ESLint
npm run lint:fix # ESLint avec auto-fix
npm run format # Prettier — formater les fichiers
npm run format:check # Prettier — vérifier le formatage
npm run type-check # TypeScript — vérification des types
```
### Backend (`cd backend`)
```bash
source .venv/bin/activate # Activer le virtualenv
uvicorn app.main:app --reload # Serveur de développement (port 8000)
pytest tests/ -v # Lancer les tests
ruff check app/ tests/ # Linter
ruff format app/ tests/ # Formater le code
ruff check --fix app/ tests/ # Linter avec auto-fix
alembic upgrade head # Appliquer les migrations
alembic revision --autogenerate -m "description" # Créer une migration
```
### Infrastructure
```bash
docker compose up -d # PostgreSQL + Ollama
docker compose down # Arrêter les services
```
## Conventions
### TypeScript / Frontend
- TypeScript strict (`strict: true`), jamais de `any`
- Composants : PascalCase (`OffreCard.tsx`), fonctionnels uniquement
- Hooks : camelCase préfixé `use` (`useOffres.ts`)
- Types/Interfaces : PascalCase, pas de préfixe `I`
- Constantes : UPPER_SNAKE_CASE
- Imports : alias `@/` pour `src/`
- Un composant par fichier
### Python / Backend
- Python 3.11+, annotations de type sur toutes les fonctions publiques
- Modules et variables : snake_case, classes : PascalCase
- `async def` pour tous les endpoints et accès DB
- Imports absolus : `from app.services.matching import ...`
- Pas de `print()` — utiliser `logging`
- Pas de `# type: ignore`
### Git
- Conventional Commits en français : `feat:`, `fix:`, `chore:`
- Ne JAMAIS ajouter `Co-Authored-By` dans les messages de commit
- Branches : `feature/nom-court`, `fix/description`, `chore/description`
## Règles impératives
1. Ne jamais hardcoder de clés API — variables d'environnement via `.env`
2. Ne jamais commit `.env` — uniquement `.env.example`
3. Toujours vérifier via Context7 avant d'implémenter une intégration API externe
4. Le LLM local est optionnel — mode dégradé si Ollama indisponible
5. Mono-utilisateur pour l'instant, mais architecture extensible
6. Français par défaut pour toute l'interface
7. Responsive — optimisé desktop, utilisable mobile
## Structure
```
cvp/
├── frontend/ # Next.js 14 App Router
│ ├── src/
│ │ ├── app/[locale]/ # Pages i18n (dashboard, offres, profil, cv, candidatures, parametres)
│ │ ├── components/ # ui/ (shadcn), layout/, offres/, cv/, profil/
│ │ ├── lib/ # Utilitaires (utils.ts)
│ │ ├── hooks/ # Custom React hooks
│ │ ├── types/ # Types TypeScript partagés
│ │ ├── messages/ # Traductions (fr.json, en.json)
│ │ └── i18n/ # Config next-intl (routing.ts, request.ts)
│ └── package.json
├── backend/ # FastAPI
│ ├── app/
│ │ ├── main.py # Point d'entrée + routers
│ │ ├── config.py # Settings (Pydantic)
│ │ ├── api/routes/ # offres, profil, cv, candidatures, parametres
│ │ ├── api/deps.py # Dépendances (DbSession)
│ │ ├── models/ # SQLAlchemy
│ │ ├── schemas/ # Pydantic
│ │ ├── services/ # Logique métier
│ │ ├── templates/ # Templates CV (HTML)
│ │ └── db/ # database.py + migrations/ (Alembic)
│ ├── tests/
│ ├── requirements.txt
│ └── pyproject.toml
├── docker-compose.yml # PostgreSQL 16 + Ollama
├── .env.example
└── PROMPT.md # Spécification complète du projet
```
+119
View File
@@ -0,0 +1,119 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
# Use forward slashes (/) also on windows to provide an os agnostic path
script_location = app/db/migrations
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to app/db/migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:app/db/migrations/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
# version_path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
version_path_separator = os
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url =
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
View File
View File
+8
View File
@@ -0,0 +1,8 @@
from typing import Annotated
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.database import get_db
DbSession = Annotated[AsyncSession, Depends(get_db)]
View File
+3
View File
@@ -0,0 +1,3 @@
from fastapi import APIRouter
router = APIRouter(prefix="/candidatures", tags=["candidatures"])
+3
View File
@@ -0,0 +1,3 @@
from fastapi import APIRouter
router = APIRouter(prefix="/cv", tags=["cv"])
+3
View File
@@ -0,0 +1,3 @@
from fastapi import APIRouter
router = APIRouter(prefix="/offres", tags=["offres"])
+3
View File
@@ -0,0 +1,3 @@
from fastapi import APIRouter
router = APIRouter(prefix="/parametres", tags=["parametres"])
+3
View File
@@ -0,0 +1,3 @@
from fastapi import APIRouter
router = APIRouter(prefix="/profil", tags=["profil"])
+30
View File
@@ -0,0 +1,30 @@
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
postgres_host: str = "localhost"
postgres_port: int = 5432
postgres_db: str = "cvp"
postgres_user: str = "cvp"
postgres_password: str = "cvp_dev_password"
france_travail_client_id: str = ""
france_travail_client_secret: str = ""
adzuna_app_id: str = ""
adzuna_app_key: str = ""
ollama_base_url: str = "http://localhost:11434"
ollama_model: str = "phi3:mini"
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
@property
def database_url(self) -> str:
return (
f"postgresql+asyncpg://{self.postgres_user}:{self.postgres_password}"
f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}"
)
settings = Settings()
View File
+11
View File
@@ -0,0 +1,11 @@
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from app.config import settings
engine = create_async_engine(settings.database_url, echo=False)
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async def get_db() -> AsyncSession:
async with async_session() as session:
yield session
+1
View File
@@ -0,0 +1 @@
Generic single-database configuration.
+54
View File
@@ -0,0 +1,54 @@
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
from app.config import settings
# This is the Alembic Config object
config = context.config
# Set the SQLAlchemy URL from our app config
# Use the sync version (postgresql:// instead of postgresql+asyncpg://)
config.set_main_option(
"sqlalchemy.url",
settings.database_url.replace("+asyncpg", ""),
)
# Interpret the config file for Python logging
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# Import all models here so Alembic can detect them
# from app.models import Base # uncomment when models are defined
target_metadata = None # replace with Base.metadata when models are defined
def run_migrations_offline() -> None:
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
+28
View File
@@ -0,0 +1,28 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}
+35
View File
@@ -0,0 +1,35 @@
import logging
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.routes import candidatures, cv, offres, parametres, profil
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(
title="CVP API",
description="API pour la recherche d'emploi intelligente",
version="0.1.0",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/health")
async def health_check() -> dict[str, str]:
return {"status": "ok"}
app.include_router(offres.router, prefix="/api")
app.include_router(profil.router, prefix="/api")
app.include_router(cv.router, prefix="/api")
app.include_router(candidatures.router, prefix="/api")
app.include_router(parametres.router, prefix="/api")
View File
View File
View File
View File
+20
View File
@@ -0,0 +1,20 @@
[project]
name = "cvp-backend"
version = "0.1.0"
description = "CVP - Backend API pour la recherche d'emploi intelligente"
requires-python = ">=3.11"
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
[tool.ruff]
target-version = "py311"
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "UP", "ANN", "B", "A", "SIM"]
ignore = []
[tool.ruff.lint.isort]
known-first-party = ["app"]
+21
View File
@@ -0,0 +1,21 @@
# Core
fastapi==0.115.12
uvicorn[standard]==0.34.2
sqlalchemy[asyncio]==2.0.41
asyncpg==0.30.0
psycopg2-binary==2.9.11
alembic==1.15.2
pydantic==2.11.3
pydantic-settings==2.9.1
python-dotenv==1.1.0
httpx==0.28.1
apscheduler==3.11.0
# weasyprint==65.1 # skipped: requires system libs (libpango, libcairo, etc.)
python-docx==1.1.2
ollama==0.4.8
# Dev
pytest==8.3.5
pytest-asyncio==0.25.3
pytest-cov==6.1.1
ruff==0.11.8
View File
+11
View File
@@ -0,0 +1,11 @@
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_health_check() -> None:
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
+29
View File
@@ -0,0 +1,29 @@
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: cvp
POSTGRES_USER: cvp
POSTGRES_PASSWORD: cvp_dev_password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
ollama:
image: ollama/ollama:latest
ports:
- "11434:11434"
volumes:
- ollama_data:/root/.ollama
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
volumes:
postgres_data:
ollama_data:
@@ -0,0 +1,746 @@
# Phase 1 — Fondations : Scaffolding du projet CVP
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Initialiser le monorepo CVP avec un frontend Next.js 14 et un backend FastAPI, configurer le tooling (lint, format, tests), créer la structure de dossiers, et produire un commit initial propre.
**Architecture:** Monorepo avec deux sous-projets : `frontend/` (Next.js 14 App Router, TypeScript strict, Tailwind 3, shadcn/ui) et `backend/` (FastAPI, SQLAlchemy 2, Alembic, Python 3.11). Docker Compose pour PostgreSQL 16 et Ollama. Les deux sous-projets sont indépendants et communiquent via REST API.
**Tech Stack:** Next.js 14, React 18, TypeScript, Tailwind CSS 3, shadcn/ui, next-intl, FastAPI, SQLAlchemy 2, Alembic, Pydantic 2, PostgreSQL 16, Python 3.11
---
## Prérequis
- Python 3.11 doit etre installé (`sudo dnf install -y python3.11 python3.11-pip python3.11-devel`)
- Node.js 22+ et npm 10+ sont déjà disponibles
- PostgreSQL 16 sera lancé via Docker Compose
---
## Task 1 : Fichiers racine du monorepo
**Files:**
- Create: `.gitignore`
- Create: `.env.example`
- Create: `docker-compose.yml`
- [ ] **Step 1: Créer le `.gitignore`**
```gitignore
# Dependencies
node_modules/
__pycache__/
*.pyc
.venv/
venv/
# Environment
.env
.env.local
.env.*.local
# Build
.next/
out/
dist/
build/
*.egg-info/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Testing
coverage/
htmlcov/
.pytest_cache/
.coverage
# Database
*.db
*.sqlite3
# Generated
*.pdf
*.docx
```
- [ ] **Step 2: Créer le `.env.example`**
```env
# PostgreSQL
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=cvp
POSTGRES_USER=cvp
POSTGRES_PASSWORD=cvp_dev_password
# France Travail API
FRANCE_TRAVAIL_CLIENT_ID=
FRANCE_TRAVAIL_CLIENT_SECRET=
# Adzuna API
ADZUNA_APP_ID=
ADZUNA_APP_KEY=
# Ollama
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=phi3:mini
# Backend
BACKEND_HOST=0.0.0.0
BACKEND_PORT=8000
# Frontend
NEXT_PUBLIC_API_URL=http://localhost:8000
```
- [ ] **Step 3: Créer le `docker-compose.yml`**
```yaml
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: cvp
POSTGRES_USER: cvp
POSTGRES_PASSWORD: cvp_dev_password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
ollama:
image: ollama/ollama:latest
ports:
- "11434:11434"
volumes:
- ollama_data:/root/.ollama
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
volumes:
postgres_data:
ollama_data:
```
---
## Task 2 : Initialiser le frontend Next.js
**Files:**
- Create: `frontend/` (via `create-next-app`)
- Modify: `frontend/tsconfig.json`
- Modify: `frontend/package.json`
- [ ] **Step 1: Créer le projet Next.js 14**
```bash
cd /home/localadm/cvp
npx create-next-app@14 frontend \
--typescript \
--tailwind \
--eslint \
--app \
--src-dir \
--import-alias "@/*" \
--no-turbopack
```
- [ ] **Step 2: Vérifier que TypeScript strict est activé**
Dans `frontend/tsconfig.json`, s'assurer que `"strict": true` est présent dans `compilerOptions`.
- [ ] **Step 3: Vérifier que l'app démarre**
```bash
cd /home/localadm/cvp/frontend
npm run dev &
sleep 5
curl -s http://localhost:3000 | head -20
kill %1
```
Expected: réponse HTML valide.
---
## Task 3 : Configurer Tailwind, shadcn/ui et next-intl dans le frontend
**Files:**
- Modify: `frontend/tailwind.config.ts` (thème CVP)
- Modify: `frontend/src/app/globals.css`
- Create: `frontend/components.json` (via shadcn init)
- Create: `frontend/src/messages/fr.json`
- Create: `frontend/src/messages/en.json`
- Create: `frontend/src/i18n/request.ts`
- Create: `frontend/src/i18n/routing.ts`
- Modify: `frontend/next.config.js` (plugin next-intl)
- Modify: `frontend/package.json` (ajout next-intl)
- [ ] **Step 1: Installer et configurer shadcn/ui**
```bash
cd /home/localadm/cvp/frontend
npx shadcn@latest init -d
```
- [ ] **Step 2: Configurer le thème Tailwind CVP**
Modifier `frontend/tailwind.config.ts` pour ajouter les couleurs du design system :
- Primaire : indigo (#6366F1)
- Secondaire : cyan (#06B6D4)
- Accent : orange (#F59E0B)
- Succès : vert (#10B981)
- Danger : rouge (#EF4444)
- Fond dark : #0F172A / #1E293B
- Polices : Inter (UI) + JetBrains Mono (code)
- `borderRadius` : `xl` par défaut
- [ ] **Step 3: Installer et configurer next-intl**
```bash
cd /home/localadm/cvp/frontend
npm install next-intl
```
Créer `src/messages/fr.json` :
```json
{
"common": {
"appName": "CVP",
"loading": "Chargement...",
"error": "Une erreur est survenue",
"save": "Enregistrer",
"cancel": "Annuler",
"delete": "Supprimer",
"edit": "Modifier",
"back": "Retour"
},
"nav": {
"dashboard": "Tableau de bord",
"offres": "Offres",
"profil": "Profil",
"cv": "CV",
"candidatures": "Candidatures",
"parametres": "Parametres"
}
}
```
Créer `src/messages/en.json` avec les traductions anglaises correspondantes.
Créer `src/i18n/routing.ts` :
```typescript
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
locales: ["fr", "en"],
defaultLocale: "fr",
});
```
Créer `src/i18n/request.ts` :
```typescript
import { getRequestConfig } from "next-intl/server";
import { routing } from "./routing";
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
if (!locale || !routing.locales.includes(locale as "fr" | "en")) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
});
```
Mettre a jour `next.config.js` avec le plugin `createNextIntlPlugin`.
- [ ] **Step 4: Restructurer le App Router pour i18n**
Déplacer les pages sous `src/app/[locale]/` et créer le layout racine avec `NextIntlClientProvider`. Créer le middleware `src/middleware.ts` pour la redirection de locale.
---
## Task 4 : Créer la structure de dossiers frontend
**Files:**
- Create: `frontend/src/components/ui/.gitkeep`
- Create: `frontend/src/components/layout/.gitkeep`
- Create: `frontend/src/components/offres/.gitkeep`
- Create: `frontend/src/components/cv/.gitkeep`
- Create: `frontend/src/components/profil/.gitkeep`
- Create: `frontend/src/lib/.gitkeep`
- Create: `frontend/src/hooks/.gitkeep`
- Create: `frontend/src/types/.gitkeep`
- Create: `frontend/src/app/[locale]/dashboard/page.tsx`
- Create: `frontend/src/app/[locale]/offres/page.tsx`
- Create: `frontend/src/app/[locale]/profil/page.tsx`
- Create: `frontend/src/app/[locale]/cv/page.tsx`
- Create: `frontend/src/app/[locale]/candidatures/page.tsx`
- Create: `frontend/src/app/[locale]/parametres/page.tsx`
- [ ] **Step 1: Créer les dossiers de composants**
Créer les sous-dossiers dans `components/` avec des `.gitkeep` pour les dossiers vides.
- [ ] **Step 2: Créer les pages placeholder**
Chaque page sous `src/app/[locale]/` est un simple composant avec le titre de la section :
```tsx
// src/app/[locale]/dashboard/page.tsx
export default function DashboardPage() {
return (
<main className="p-8">
<h1 className="text-3xl font-bold">Tableau de bord</h1>
</main>
);
}
```
Meme pattern pour : `offres/page.tsx`, `profil/page.tsx`, `cv/page.tsx`, `candidatures/page.tsx`, `parametres/page.tsx`.
---
## Task 5 : Configurer ESLint et Prettier pour le frontend
**Files:**
- Modify: `frontend/.eslintrc.json`
- Create: `frontend/.prettierrc`
- Modify: `frontend/package.json` (scripts)
- [ ] **Step 1: Installer Prettier**
```bash
cd /home/localadm/cvp/frontend
npm install -D prettier eslint-config-prettier
```
- [ ] **Step 2: Créer `.prettierrc`**
```json
{
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100
}
```
- [ ] **Step 3: Configurer ESLint**
Mettre a jour `.eslintrc.json` pour inclure `prettier` dans `extends` et ajouter les regles : `no-console: warn`, `@typescript-eslint/no-explicit-any: error`.
- [ ] **Step 4: Ajouter les scripts npm**
Dans `package.json`, ajouter :
```json
{
"scripts": {
"lint": "next lint",
"lint:fix": "next lint --fix",
"format": "prettier --write \"src/**/*.{ts,tsx,json,css}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,json,css}\"",
"type-check": "tsc --noEmit"
}
}
```
- [ ] **Step 5: Vérifier que lint passe**
```bash
cd /home/localadm/cvp/frontend
npm run lint
npm run format:check
npm run type-check
```
Expected: pas d'erreurs.
---
## Task 6 : Initialiser le backend Python
**Files:**
- Create: `backend/pyproject.toml`
- Create: `backend/requirements.txt`
- Create: `backend/app/__init__.py`
- Create: `backend/app/main.py`
- Create: `backend/app/config.py`
- [ ] **Step 1: Installer Python 3.11 et créer le virtualenv**
```bash
sudo dnf install -y python3.11 python3.11-pip python3.11-devel
cd /home/localadm/cvp/backend
python3.11 -m venv .venv
source .venv/bin/activate
```
- [ ] **Step 2: Créer `pyproject.toml`**
```toml
[project]
name = "cvp-backend"
version = "0.1.0"
description = "CVP - Backend API pour la recherche d'emploi intelligente"
requires-python = ">=3.11"
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
[tool.ruff]
target-version = "py311"
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "UP", "ANN", "B", "A", "SIM"]
ignore = ["ANN101", "ANN102"]
[tool.ruff.lint.isort]
known-first-party = ["app"]
```
- [ ] **Step 3: Créer `requirements.txt`**
```txt
fastapi==0.115.12
uvicorn[standard]==0.34.2
sqlalchemy[asyncio]==2.0.41
asyncpg==0.30.0
alembic==1.15.2
pydantic==2.11.3
pydantic-settings==2.9.1
python-dotenv==1.1.0
httpx==0.28.1
apscheduler==3.11.0
weasyprint==65.1
python-docx==1.1.2
ollama==0.4.8
# Dev
pytest==8.3.5
pytest-asyncio==0.25.3
pytest-cov==6.1.1
ruff==0.11.8
httpx==0.28.1
```
- [ ] **Step 4: Installer les dépendances**
```bash
cd /home/localadm/cvp/backend
source .venv/bin/activate
pip install -r requirements.txt
```
- [ ] **Step 5: Créer le point d'entrée FastAPI**
`backend/app/__init__.py` : fichier vide.
`backend/app/config.py` :
```python
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
postgres_host: str = "localhost"
postgres_port: int = 5432
postgres_db: str = "cvp"
postgres_user: str = "cvp"
postgres_password: str = "cvp_dev_password"
france_travail_client_id: str = ""
france_travail_client_secret: str = ""
adzuna_app_id: str = ""
adzuna_app_key: str = ""
ollama_base_url: str = "http://localhost:11434"
ollama_model: str = "phi3:mini"
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
@property
def database_url(self) -> str:
return (
f"postgresql+asyncpg://{self.postgres_user}:{self.postgres_password}"
f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}"
)
settings = Settings()
```
`backend/app/main.py` :
```python
import logging
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(
title="CVP API",
description="API pour la recherche d'emploi intelligente",
version="0.1.0",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/health")
async def health_check() -> dict[str, str]:
return {"status": "ok"}
```
- [ ] **Step 6: Vérifier que le backend démarre**
```bash
cd /home/localadm/cvp/backend
source .venv/bin/activate
uvicorn app.main:app --host 0.0.0.0 --port 8000 &
sleep 3
curl -s http://localhost:8000/health
kill %1
```
Expected: `{"status":"ok"}`
---
## Task 7 : Créer la structure de dossiers backend
**Files:**
- Create: `backend/app/api/__init__.py`
- Create: `backend/app/api/routes/__init__.py`
- Create: `backend/app/api/routes/offres.py`
- Create: `backend/app/api/routes/profil.py`
- Create: `backend/app/api/routes/cv.py`
- Create: `backend/app/api/routes/candidatures.py`
- Create: `backend/app/api/routes/parametres.py`
- Create: `backend/app/api/deps.py`
- Create: `backend/app/models/__init__.py`
- Create: `backend/app/schemas/__init__.py`
- Create: `backend/app/services/__init__.py`
- Create: `backend/app/templates/.gitkeep`
- Create: `backend/app/db/__init__.py`
- Create: `backend/app/db/database.py`
- Create: `backend/tests/__init__.py`
- Create: `backend/tests/test_health.py`
- [ ] **Step 1: Créer les packages Python**
Créer tous les dossiers avec `__init__.py` vides pour : `api/`, `api/routes/`, `models/`, `schemas/`, `services/`, `db/`, `tests/`.
- [ ] **Step 2: Créer les routers placeholder**
Chaque fichier de route est un router minimal :
```python
# backend/app/api/routes/offres.py
from fastapi import APIRouter
router = APIRouter(prefix="/offres", tags=["offres"])
```
Meme pattern pour `profil.py`, `cv.py`, `candidatures.py`, `parametres.py`.
- [ ] **Step 3: Créer le module database**
```python
# backend/app/db/database.py
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from app.config import settings
engine = create_async_engine(settings.database_url, echo=False)
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async def get_db() -> AsyncSession:
async with async_session() as session:
yield session
```
- [ ] **Step 4: Créer le module deps**
```python
# backend/app/api/deps.py
from typing import Annotated
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.database import get_db
DbSession = Annotated[AsyncSession, Depends(get_db)]
```
- [ ] **Step 5: Enregistrer les routers dans main.py**
Ajouter dans `app/main.py` :
```python
from app.api.routes import offres, profil, cv, candidatures, parametres
app.include_router(offres.router, prefix="/api")
app.include_router(profil.router, prefix="/api")
app.include_router(cv.router, prefix="/api")
app.include_router(candidatures.router, prefix="/api")
app.include_router(parametres.router, prefix="/api")
```
- [ ] **Step 6: Écrire le premier test**
```python
# backend/tests/test_health.py
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_health_check():
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
```
- [ ] **Step 7: Lancer le test**
```bash
cd /home/localadm/cvp/backend
source .venv/bin/activate
pytest tests/test_health.py -v
```
Expected: 1 test PASSED.
---
## Task 8 : Configurer Ruff (lint + format) pour le backend
**Files:**
- Modify: `backend/pyproject.toml` (déjà fait en Task 6)
- [ ] **Step 1: Vérifier que ruff lint passe**
```bash
cd /home/localadm/cvp/backend
source .venv/bin/activate
ruff check app/ tests/
```
Expected: pas d'erreurs (corriger si nécessaire).
- [ ] **Step 2: Vérifier que ruff format passe**
```bash
ruff format --check app/ tests/
```
Expected: pas de diff (corriger si nécessaire avec `ruff format app/ tests/`).
---
## Task 9 : Configurer Alembic pour les migrations
**Files:**
- Create: `backend/alembic.ini`
- Create: `backend/app/db/migrations/env.py`
- Create: `backend/app/db/migrations/versions/.gitkeep`
- Create: `backend/app/db/migrations/script.py.mako`
- [ ] **Step 1: Initialiser Alembic**
```bash
cd /home/localadm/cvp/backend
source .venv/bin/activate
alembic init app/db/migrations
```
- [ ] **Step 2: Configurer `alembic.ini`**
Mettre `sqlalchemy.url` a vide (sera fourni par `env.py` dynamiquement).
- [ ] **Step 3: Configurer `env.py`**
Modifier `app/db/migrations/env.py` pour :
- Importer `settings` depuis `app.config`
- Utiliser `settings.database_url` comme URL de connexion
- Importer le `Base` de SQLAlchemy pour l'autogénération des migrations
---
## Task 10 : Générer le CLAUDE.md
**Files:**
- Create: `CLAUDE.md`
- [ ] **Step 1: Créer le CLAUDE.md a la racine**
Le fichier doit contenir :
- Description du projet
- Commandes utiles (dev, test, lint, format, build)
- Conventions de codage (résumé du PROMPT.md)
- Structure du projet
- Regles impératives
---
## Task 11 : Commit initial
- [ ] **Step 1: Vérifier que tout fonctionne**
```bash
# Frontend
cd /home/localadm/cvp/frontend
npm run lint
npm run type-check
npm run build
# Backend
cd /home/localadm/cvp/backend
source .venv/bin/activate
ruff check app/ tests/
pytest tests/ -v
```
Expected: tout passe sans erreur.
- [ ] **Step 2: Commit initial**
```bash
cd /home/localadm/cvp
git add .
git commit -m "feat: initialiser le projet CVP (Next.js 14 + FastAPI + PostgreSQL)"
```
Note : ne PAS ajouter `Co-Authored-By`.
+7
View File
@@ -0,0 +1,7 @@
{
"extends": ["next/core-web-vitals", "next/typescript", "prettier"],
"rules": {
"no-console": "warn",
"@typescript-eslint/no-explicit-any": "error"
}
}
+36
View File
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
+7
View File
@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100
}
+36
View File
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+20
View File
@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
+8
View File
@@ -0,0 +1,8 @@
import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default withNextIntl(nextConfig);
+6773
View File
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix",
"format": "prettier --write \"src/**/*.{ts,tsx,json,css}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,json,css}\"",
"type-check": "tsc --noEmit"
},
"dependencies": {
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.8.0",
"next": "14.2.35",
"next-intl": "^4.9.1",
"react": "^18",
"react-dom": "^18",
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.35",
"eslint-config-prettier": "^10.1.8",
"postcss": "^8",
"prettier": "^3.8.2",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
+8
View File
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;
@@ -0,0 +1,7 @@
export default function CandidaturesPage() {
return (
<main className="p-8">
<h1 className="text-3xl font-bold">Candidatures</h1>
</main>
);
}
+7
View File
@@ -0,0 +1,7 @@
export default function CvPage() {
return (
<main className="p-8">
<h1 className="text-3xl font-bold">Mes CV</h1>
</main>
);
}
@@ -0,0 +1,7 @@
export default function DashboardPage() {
return (
<main className="p-8">
<h1 className="text-3xl font-bold">Tableau de bord</h1>
</main>
);
}
+21
View File
@@ -0,0 +1,21 @@
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import { notFound } from "next/navigation";
import { routing } from "@/i18n/routing";
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
if (!routing.locales.includes(locale as "fr" | "en")) {
notFound();
}
const messages = await getMessages();
return <NextIntlClientProvider messages={messages}>{children}</NextIntlClientProvider>;
}
@@ -0,0 +1,7 @@
export default function OffresPage() {
return (
<main className="p-8">
<h1 className="text-3xl font-bold">Offres d&apos;emploi</h1>
</main>
);
}
+10
View File
@@ -0,0 +1,10 @@
export default function HomePage() {
return (
<main className="p-8">
<h1 className="text-3xl font-bold">CVP CV Personnalisé</h1>
<p className="mt-2 text-muted-foreground">
Application de recherche d&apos;emploi intelligente
</p>
</main>
);
}
@@ -0,0 +1,7 @@
export default function ParametresPage() {
return (
<main className="p-8">
<h1 className="text-3xl font-bold">Paramètres</h1>
</main>
);
}
@@ -0,0 +1,7 @@
export default function ProfilPage() {
return (
<main className="p-8">
<h1 className="text-3xl font-bold">Mon profil</h1>
</main>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.
Binary file not shown.
+102
View File
@@ -0,0 +1,102 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
@layer base {
:root {
/* CVP Design System — Light Mode */
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
/* Primary: Indigo #6366F1 */
--primary: 239 84% 67%;
--primary-foreground: 0 0% 98%;
/* Secondary: Cyan #06B6D4 */
--secondary: 192 91% 43%;
--secondary-foreground: 0 0% 98%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
/* Accent: Orange #F59E0B */
--accent: 38 92% 50%;
--accent-foreground: 222.2 47.4% 11.2%;
/* Destructive: Red #EF4444 */
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
/* Success: Green #10B981 */
--success: 160 84% 39%;
--success-foreground: 0 0% 100%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 239 84% 67%;
/* Radius: rounded-xl = 0.75rem */
--radius: 0.75rem;
}
.dark {
/* CVP Design System — Dark Mode */
/* Background: #0F172A */
--background: 222 47% 11%;
--foreground: 0 0% 98%;
/* Card: #1E293B */
--card: 217 33% 17%;
--card-foreground: 0 0% 98%;
--popover: 217 33% 17%;
--popover-foreground: 0 0% 98%;
/* Primary: Indigo #6366F1 */
--primary: 239 84% 67%;
--primary-foreground: 0 0% 98%;
/* Secondary: Cyan #06B6D4 */
--secondary: 192 91% 43%;
--secondary-foreground: 0 0% 98%;
--muted: 217 33% 17%;
--muted-foreground: 215 20% 65%;
/* Accent: Orange #F59E0B */
--accent: 38 92% 50%;
--accent-foreground: 222.2 47.4% 11.2%;
/* Destructive: Red #EF4444 */
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
/* Success: Green #10B981 */
--success: 160 84% 39%;
--success-foreground: 0 0% 100%;
--border: 217 33% 17%;
--input: 217 33% 17%;
--ring: 239 84% 67%;
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
+19
View File
@@ -0,0 +1,19 @@
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "CVP — CV Personnalisé",
description: "Application de recherche d'emploi intelligente",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="fr">
<body className="antialiased">{children}</body>
</html>
);
}
+6
View File
@@ -0,0 +1,6 @@
// This file is intentionally empty.
// The middleware in src/middleware.ts redirects all routes to the [locale] prefix.
// e.g., / → /fr, /about → /fr/about
export default function RootPage() {
return null;
}
View File
View File
+13
View File
@@ -0,0 +1,13 @@
import { getRequestConfig } from "next-intl/server";
import { routing } from "./routing";
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
if (!locale || !routing.locales.includes(locale as "fr" | "en")) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
});
+6
View File
@@ -0,0 +1,6 @@
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
locales: ["fr", "en"],
defaultLocale: "fr",
});
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+20
View File
@@ -0,0 +1,20 @@
{
"common": {
"appName": "CVP",
"loading": "Loading...",
"error": "An error occurred",
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"back": "Back"
},
"nav": {
"dashboard": "Dashboard",
"offres": "Job Offers",
"profil": "Profile",
"cv": "Resume",
"candidatures": "Applications",
"parametres": "Settings"
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"common": {
"appName": "CVP",
"loading": "Chargement...",
"error": "Une erreur est survenue",
"save": "Enregistrer",
"cancel": "Annuler",
"delete": "Supprimer",
"edit": "Modifier",
"back": "Retour"
},
"nav": {
"dashboard": "Tableau de bord",
"offres": "Offres",
"profil": "Profil",
"cv": "CV",
"candidatures": "Candidatures",
"parametres": "Paramètres"
}
}
+8
View File
@@ -0,0 +1,8 @@
import createMiddleware from "next-intl/middleware";
import { routing } from "./i18n/routing";
export default createMiddleware(routing);
export const config = {
matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};
View File
+65
View File
@@ -0,0 +1,65 @@
import type { Config } from "tailwindcss";
const config: Config = {
darkMode: "class",
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
success: {
DEFAULT: "hsl(var(--success))",
foreground: "hsl(var(--success-foreground))",
},
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
xl: "var(--radius)",
},
fontFamily: {
sans: ["Inter", "ui-sans-serif", "system-ui", "sans-serif"],
mono: ["JetBrains Mono", "ui-monospace", "monospace"],
},
},
},
plugins: [require("tailwindcss-animate")],
};
export default config;
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}