Météo Exam v0.1.0 — calendrier des évaluations en bulletin météo

- API Express + SQLite : classes, évaluations, SSE, auth Bearer
- Front Vite + TS + Tailwind : frise 7 jours, Exam Cards, modales
- Capacitor Android : APK debug, CapacitorHttp natif
- Seed : classe Terminale S2 (code TERMS2)
This commit is contained in:
Mathis 2026-10-02 19:51:13 +02:00
commit 6d154ed5fb
92 changed files with 9574 additions and 0 deletions

18
shared/package.json Normal file
View file

@ -0,0 +1,18 @@
{
"name": "@meteo-exam/shared",
"private": true,
"version": "0.1.0",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"build": "tsc --noEmit",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"typescript": "^5.6.0"
}
}

175
shared/src/index.ts Normal file
View file

@ -0,0 +1,175 @@
/**
* Types et métadonnées partagés entre le serveur et le front.
* Aucun import runtime ici : ce module est consommé tel quel par les deux côtés.
*/
/** Statut météorologique d'une évaluation. */
export type WeatherKind = 'soleil' | 'brouillard' | 'vent' | 'orage' | 'tempete'
export const WEATHER_KINDS: WeatherKind[] = ['soleil', 'brouillard', 'vent', 'orage', 'tempete']
/** Du plus rassurant au plus menaçant. Sert aux tris et au calcul du statut d'une journée. */
export const SEVERITY: Record<WeatherKind, number> = {
soleil: 0,
brouillard: 1,
vent: 2,
orage: 3,
tempete: 4,
}
export interface WeatherMeta {
/** Nom affiché, ex. « Alerte Rouge ». */
label: string
/** Titre long pour les modales et la légende. */
title: string
icon: string
/** Description pédagogique affichée dans la légende. */
hint: string
/** Couleur d'accent en dur, pour les SVG et jauges. */
hex: string
/** Classes Tailwind de la pastille (fond / bordure / texte). */
chip: string
/** Classe de couleur du texte seule, pour les libellés sur fond neutre. */
accent: string
/** Classes Tailwind du badge plein. */
badge: string
}
export const WEATHER: Record<WeatherKind, WeatherMeta> = {
soleil: {
label: 'Grand Soleil',
title: 'Grand Soleil',
icon: '☀️',
hint: 'Journée calme. Aucun examen répertorié ni suspecté.',
hex: '#0284c7',
chip: 'bg-sky-500/10 border-sky-500/20 text-sky-400',
accent: 'text-sky-400',
badge: 'bg-sky-500/15 text-sky-300 border-sky-400/30',
},
brouillard: {
label: 'Brouillard',
title: 'Brouillard',
icon: '🌫️',
hint: 'Zone d’incertitude. Rumeur ou sous-entendu d’un professeur.',
hex: '#7c3aed',
chip: 'bg-purple-500/10 border-purple-500/20 text-purple-400',
accent: 'text-purple-400',
badge: 'bg-purple-500/15 text-purple-300 border-purple-400/30',
},
vent: {
label: 'Coup de vent',
title: 'Coup de vent',
icon: '💨',
hint: 'Interrogation surprise potentielle ou petit QCM, coefficient faible.',
hex: '#d97706',
chip: 'bg-amber-500/10 border-amber-500/20 text-amber-400',
accent: 'text-amber-400',
badge: 'bg-amber-500/15 text-amber-300 border-amber-400/30',
},
orage: {
label: 'Orage',
title: 'Orage / Pluie battante',
icon: '🌩️',
hint: 'Devoir surveillé (DS) confirmé ou évaluation de chapitre.',
hex: '#e11d48',
chip: 'bg-rose-500/10 border-rose-500/20 text-rose-400',
accent: 'text-rose-400',
badge: 'bg-rose-500/15 text-rose-300 border-rose-400/30',
},
tempete: {
label: 'Alerte Rouge',
title: 'Alerte Rouge / Tempête',
icon: '🚨',
hint: 'Examen blanc, épreuve commune ou gros devoir de fin de trimestre.',
hex: '#dc2626',
chip: 'bg-red-500/10 border-red-500/20 text-red-500',
accent: 'text-red-500',
badge: 'bg-red-500/15 text-red-300 border-red-400/30',
},
}
export function isWeatherKind(value: unknown): value is WeatherKind {
return typeof value === 'string' && (WEATHER_KINDS as string[]).includes(value)
}
/**
* Statut par défaut déduit du coefficient, pour pré-remplir le formulaire d'ajout.
* 0 = rien de prévu, 1 = interro surprise, 2-3 = DS, 4+ = alerte rouge.
*/
export function kindFromWeight(weight: number): WeatherKind {
if (weight <= 0) return 'soleil'
if (weight < 2) return 'vent'
if (weight < 4) return 'orage'
return 'tempete'
}
/** Les coefficients proposés dans le formulaire, avec leur statut déduit. */
export const WEIGHT_PRESETS: { value: number; label: string }[] = [
{ value: 0, label: '0 — coursework' },
{ value: 1, label: '1 — interro' },
{ value: 2, label: '2 — contrôle' },
{ value: 3, label: '3 — DS' },
{ value: 4, label: '4 — examen blanc' },
{ value: 5, label: '5 — épreuve commune' },
]
/** Statut le plus sévère d'une journée ; `soleil` si la journée est vide. */
export function dominantKind(exams: { kind: WeatherKind }[]): WeatherKind {
let best: WeatherKind = 'soleil'
for (const exam of exams) {
if (SEVERITY[exam.kind] > SEVERITY[best]) best = exam.kind
}
return best
}
export type Role = 'admin' | 'student'
export interface Exam {
id: number
classId: number
subject: string
teacher: string | null
room: string | null
chapter: string | null
/** ISO 8601 UTC. */
startsAt: string
durationMin: number
/** Coefficient : alimente la future « pression atmosphérique ». */
weight: number
kind: WeatherKind
note: string | null
createdAt: string
updatedAt: string
}
export interface ClassInfo {
id: number
name: string
level: string | null
}
export interface Member {
id: number
classId: number
nickname: string
role: Role
}
/** Charge utile de `GET /api/me`, renvoyée aussi à la création et à l'adhésion. */
export interface Session {
member: Member
klass: ClassInfo
}
export interface ApiError {
error: string
}
/** Payloads émis par le flux SSE. */
export type StreamEvent =
| { type: 'exam.created'; exam: Exam }
| { type: 'exam.updated'; exam: Exam }
| { type: 'exam.deleted'; examId: number }
| { type: 'ping' }
export const CLASS_CODE_LENGTH = 6

18
shared/tsconfig.json Normal file
View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"isolatedModules": true,
"noEmit": true
},
"include": ["src"]
}