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:
@@ -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
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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)]
|
||||
@@ -0,0 +1,3 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter(prefix="/candidatures", tags=["candidatures"])
|
||||
@@ -0,0 +1,3 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter(prefix="/cv", tags=["cv"])
|
||||
@@ -0,0 +1,3 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter(prefix="/offres", tags=["offres"])
|
||||
@@ -0,0 +1,3 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter(prefix="/parametres", tags=["parametres"])
|
||||
@@ -0,0 +1,3 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter(prefix="/profil", tags=["profil"])
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
||||
@@ -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()
|
||||
@@ -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"}
|
||||
@@ -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")
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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"}
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript", "prettier"],
|
||||
"rules": {
|
||||
"no-console": "warn",
|
||||
"@typescript-eslint/no-explicit-any": "error"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
Generated
+6773
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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'emploi</h1>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -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'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.
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defineRouting } from "next-intl/routing";
|
||||
|
||||
export const routing = defineRouting({
|
||||
locales: ["fr", "en"],
|
||||
defaultLocale: "fr",
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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|.*\\..*).*)"],
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user