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