feat: Setup projet Classeo avec infrastructure Docker et architecture DDD

Configure l'environnement de développement complet avec Docker Compose,
structure DDD 4 Bounded Contexts, et pipeline CI/CD GitHub Actions.

Corrections compatibilité CI:
- Symfony 8 nécessite monolog-bundle ^4.0 (la v3.x ne supporte que jusqu'à Symfony 7)
- ESLint v9 nécessite flat config (eslint.config.js) - le format .eslintrc.cjs est obsolète
This commit is contained in:
2026-01-30 09:55:58 +01:00
parent ddefa927c7
commit 6da5996340
125 changed files with 10032 additions and 0 deletions

37
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,37 @@
# =============================================================================
# Dependencies
# =============================================================================
node_modules/
.pnpm-store/
# =============================================================================
# Build output
# =============================================================================
/.svelte-kit/
/build/
dist/
# =============================================================================
# Environment files
# =============================================================================
.env
.env.*
!.env.example
# =============================================================================
# Testing
# =============================================================================
/coverage/
/playwright-report/
/test-results/
# =============================================================================
# PWA
# =============================================================================
dev-dist/
# =============================================================================
# Misc
# =============================================================================
*.local
*.tsbuildinfo

15
frontend/.prettierrc Normal file
View File

@@ -0,0 +1,15 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

61
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,61 @@
# syntax=docker/dockerfile:1
# =============================================================================
# Node.js 22 - Frontend Classeo (SvelteKit)
# =============================================================================
FROM node:22-alpine AS base
# Install pnpm
RUN corepack enable && corepack prepare pnpm@10.28.2 --activate
# Set working directory
WORKDIR /app
# =============================================================================
# Development stage
# =============================================================================
FROM base AS dev
# Create entrypoint script for dev (installs deps if needed)
RUN echo '#!/bin/sh' > /usr/local/bin/docker-entrypoint.sh && \
echo 'set -e' >> /usr/local/bin/docker-entrypoint.sh && \
echo 'if [ ! -d /app/node_modules ] || [ ! -f /app/node_modules/.pnpm/lock.yaml ]; then' >> /usr/local/bin/docker-entrypoint.sh && \
echo ' echo "Installing pnpm dependencies..."' >> /usr/local/bin/docker-entrypoint.sh && \
echo ' pnpm install' >> /usr/local/bin/docker-entrypoint.sh && \
echo 'fi' >> /usr/local/bin/docker-entrypoint.sh && \
echo 'exec "$@"' >> /usr/local/bin/docker-entrypoint.sh && \
chmod +x /usr/local/bin/docker-entrypoint.sh
EXPOSE 5173
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD ["pnpm", "run", "dev", "--host", "0.0.0.0"]
# =============================================================================
# Build stage
# =============================================================================
FROM base AS builder
COPY package.json pnpm-lock.yaml* ./
RUN if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile; else pnpm install; fi
COPY . .
RUN pnpm run build
# =============================================================================
# Production stage
# =============================================================================
FROM base AS prod
ENV NODE_ENV=production
COPY package.json pnpm-lock.yaml* ./
RUN if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile --prod; else pnpm install --prod; fi
COPY --from=builder /app/build build/
COPY --from=builder /app/package.json .
EXPOSE 3000
CMD ["node", "build"]

21
frontend/e2e/home.test.ts Normal file
View File

@@ -0,0 +1,21 @@
import { expect, test } from '@playwright/test';
test('home page has correct title and content', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle('Classeo');
await expect(page.getByRole('heading', { name: 'Bienvenue sur Classeo' })).toBeVisible();
await expect(page.getByText('Application de gestion scolaire')).toBeVisible();
});
test('counter increments when button is clicked', async ({ page }) => {
await page.goto('/');
await expect(page.getByText('Compteur: 0')).toBeVisible();
await page.getByRole('button', { name: 'Incrementer' }).click();
await expect(page.getByText('Compteur: 1')).toBeVisible();
await page.getByRole('button', { name: 'Incrementer' }).click();
await expect(page.getByText('Compteur: 2')).toBeVisible();
});

96
frontend/eslint.config.js Normal file
View File

@@ -0,0 +1,96 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import sveltePlugin from 'eslint-plugin-svelte';
import svelteParser from 'svelte-eslint-parser';
import prettier from 'eslint-config-prettier';
export default tseslint.config(
// Base JavaScript recommended rules
js.configs.recommended,
// TypeScript recommended rules
...tseslint.configs.recommended,
// Global ignores
{
ignores: [
'.svelte-kit/**',
'build/**',
'dist/**',
'node_modules/**',
'*.config.js',
'*.config.ts'
]
},
// TypeScript files
{
files: ['**/*.ts'],
languageOptions: {
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020
},
globals: {
window: 'readonly',
document: 'readonly',
console: 'readonly',
process: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
Promise: 'readonly',
Set: 'readonly',
Map: 'readonly'
}
},
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}
],
'no-console': ['warn', { allow: ['warn', 'error'] }]
}
},
// Svelte files
{
files: ['**/*.svelte'],
languageOptions: {
parser: svelteParser,
parserOptions: {
parser: tseslint.parser,
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
globals: {
window: 'readonly',
document: 'readonly',
console: 'readonly',
process: 'readonly',
Promise: 'readonly',
Set: 'readonly',
Map: 'readonly'
}
},
plugins: {
svelte: sveltePlugin
},
rules: {
...sveltePlugin.configs.recommended.rules,
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}
]
}
},
// Prettier (disable conflicting rules)
prettier
);

55
frontend/package.json Normal file
View File

@@ -0,0 +1,55 @@
{
"name": "classeo-frontend",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:e2e": "playwright test",
"lint": "eslint .",
"format": "prettier --write ."
},
"devDependencies": {
"@playwright/test": "^1.50.0",
"@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/adapter-node": "^5.0.0",
"@sveltejs/kit": "^2.50.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"@testing-library/svelte": "^5.2.0",
"@types/node": "^22.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "^2.1.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.0.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-svelte": "^3.0.0",
"jsdom": "^27.4.0",
"postcss": "^8.4.47",
"prettier": "^3.4.0",
"prettier-plugin-svelte": "^3.3.0",
"prettier-plugin-tailwindcss": "^0.6.0",
"svelte": "^5.15.0",
"svelte-check": "^4.1.0",
"tailwindcss": "^3.4.16",
"typescript": "^5.7.0",
"typescript-eslint": "^8.54.0",
"vite": "^6.0.0",
"vitest": "^2.1.0"
},
"dependencies": {
"@tanstack/svelte-query": "^5.66.0",
"@vite-pwa/sveltekit": "^0.6.8",
"workbox-window": "^7.3.0"
},
"packageManager": "pnpm@10.28.2"
}

View File

@@ -0,0 +1,41 @@
import type { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
webServer: {
command: 'pnpm run build && pnpm run preview',
port: 4173,
reuseExistingServer: !process.env.CI
},
testDir: 'e2e',
testMatch: /(.+\.)?(test|spec)\.[jt]s/,
use: {
baseURL: 'http://localhost:4173',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},
retries: process.env.CI ? 2 : 0,
reporter: process.env.CI ? 'github' : 'html',
projects: [
{
name: 'chromium',
use: {
browserName: 'chromium'
}
},
{
name: 'firefox',
use: {
browserName: 'firefox'
}
},
{
name: 'webkit',
use: {
browserName: 'webkit'
}
}
]
};
export default config;

7144
frontend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

62
frontend/src/app.css Normal file
View File

@@ -0,0 +1,62 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--ring: 224.3 76.3% 48%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-feature-settings:
'rlig' 1,
'calt' 1;
}
}

16
frontend/src/app.html Normal file
View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#3b82f6" />
<meta name="description" content="Classeo - Application de gestion scolaire" />
<link rel="manifest" href="%sveltekit.assets%/manifest.webmanifest" />
<link rel="apple-touch-icon" href="%sveltekit.assets%/apple-touch-icon.png" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -0,0 +1,2 @@
// place files you want to import through the `$lib` alias in this folder.
export * from './types';

View File

@@ -0,0 +1,35 @@
// API response types
export interface ApiError {
code: string;
message: string;
violations?: Array<{
propertyPath: string;
message: string;
}>;
}
export interface PaginatedResponse<T> {
data: T[];
meta: {
total: number;
page: number;
itemsPerPage: number;
lastPage: number;
};
}
export interface HydraCollection<T> {
'@context': string;
'@id': string;
'@type': string;
'hydra:totalItems': number;
'hydra:member': T[];
'hydra:view'?: {
'@id': string;
'@type': string;
'hydra:first'?: string;
'hydra:last'?: string;
'hydra:next'?: string;
'hydra:previous'?: string;
};
}

View File

@@ -0,0 +1,2 @@
export * from './shared';
export * from './api';

View File

@@ -0,0 +1,27 @@
// Branded types for type safety
export type TenantId = string & { readonly brand: unique symbol };
export type UserId = string & { readonly brand: unique symbol };
export type NoteId = string & { readonly brand: unique symbol };
export type ClasseId = string & { readonly brand: unique symbol };
export type EleveId = string & { readonly brand: unique symbol };
// Helper functions for branded types
export function createTenantId(id: string): TenantId {
return id as TenantId;
}
export function createUserId(id: string): UserId {
return id as UserId;
}
export function createNoteId(id: string): NoteId {
return id as NoteId;
}
export function createClasseId(id: string): ClasseId {
return id as ClasseId;
}
export function createEleveId(id: string): EleveId {
return id as EleveId;
}

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import '../app.css';
import { browser } from '$app/environment';
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
let { children } = $props();
const queryClient = $state(
new QueryClient({
defaultOptions: {
queries: {
enabled: browser,
staleTime: 1000 * 60 * 5, // 5 minutes
retry: 1
}
}
})
);
</script>
<QueryClientProvider client={queryClient}>
{@render children()}
</QueryClientProvider>

View File

@@ -0,0 +1,28 @@
<script lang="ts">
let count = $state(0);
function increment() {
count++;
}
</script>
<svelte:head>
<title>Classeo</title>
</svelte:head>
<main class="flex min-h-screen flex-col items-center justify-center bg-gray-50">
<div class="text-center">
<h1 class="mb-4 text-4xl font-bold text-primary">Bienvenue sur Classeo</h1>
<p class="mb-8 text-gray-600">Application de gestion scolaire</p>
<div class="rounded-lg bg-white p-8 shadow-md">
<p class="mb-4 text-2xl font-semibold text-gray-800">Compteur: {count}</p>
<button
onclick={increment}
class="rounded-md bg-primary px-6 py-2 text-primary-foreground transition-colors hover:bg-primary/90"
>
Incrementer
</button>
</div>
</div>
</main>

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

21
frontend/svelte.config.js Normal file
View File

@@ -0,0 +1,21 @@
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
alias: {
$components: 'src/lib/components',
$features: 'src/lib/features',
$stores: 'src/lib/stores',
$api: 'src/lib/api',
$utils: 'src/lib/utils',
$types: 'src/lib/types'
}
}
};
export default config;

View File

@@ -0,0 +1,53 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: 'class',
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif']
}
}
},
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')]
};

View File

@@ -0,0 +1,21 @@
import { describe, it, expect } from 'vitest';
describe('Math operations', () => {
it('should add two numbers correctly', () => {
expect(1 + 1).toBe(2);
});
it('should multiply two numbers correctly', () => {
expect(2 * 3).toBe(6);
});
});
describe('String operations', () => {
it('should concatenate strings', () => {
expect('Hello' + ' ' + 'World').toBe('Hello World');
});
it('should convert to uppercase', () => {
expect('classeo'.toUpperCase()).toBe('CLASSEO');
});
});

View File

@@ -0,0 +1,23 @@
import { render, screen } from '@testing-library/svelte';
import { describe, expect, it } from 'vitest';
import Page from '../../src/routes/+page.svelte';
describe('Home Page', () => {
it('renders the welcome message', () => {
render(Page);
expect(screen.getByRole('heading', { name: 'Bienvenue sur Classeo' })).toBeTruthy();
});
it('renders the description', () => {
render(Page);
expect(screen.getByText('Application de gestion scolaire')).toBeTruthy();
});
it('starts counter at 0', () => {
render(Page);
expect(screen.getByText('Compteur: 0')).toBeTruthy();
});
});

18
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"exactOptionalPropertyTypes": true,
"moduleResolution": "bundler"
}
}

74
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,74 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { SvelteKitPWA } from '@vite-pwa/sveltekit';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [
sveltekit(),
SvelteKitPWA({
srcDir: 'src',
mode: 'development',
strategies: 'generateSW',
scope: '/',
base: '/',
manifest: {
name: 'Classeo',
short_name: 'Classeo',
description: 'Application de gestion scolaire',
theme_color: '#3b82f6',
background_color: '#ffffff',
display: 'standalone',
start_url: '/',
icons: [
{
src: 'pwa-192x192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png'
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any maskable'
}
]
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,webp,woff,woff2}']
},
devOptions: {
enabled: false,
type: 'module',
navigateFallback: '/'
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any
],
test: {
include: ['src/**/*.{test,spec}.{js,ts}', 'tests/**/*.{test,spec}.{js,ts}'],
globals: true,
environment: 'jsdom',
server: {
deps: {
inline: [/svelte/]
}
},
alias: {
$lib: '/src/lib',
$app: '/node_modules/@sveltejs/kit/src/runtime/app'
}
},
resolve: {
conditions: ['browser']
},
server: {
host: '0.0.0.0',
port: 5173,
strictPort: true
}
});