diff --git a/scripts/gen-icons.mjs b/scripts/gen-icons.mjs index 305ce59..be9e78e 100644 --- a/scripts/gen-icons.mjs +++ b/scripts/gen-icons.mjs @@ -1,6 +1,5 @@ /** - * Génère des icônes PWA simples (dégradé bleu + pastille blanche). - * Usage : node scripts/gen-icons.mjs + * Génère l'icône Météo Exam V2 : papier quadrillé + 0 rouge manuscrit. */ import { writeFileSync, mkdirSync } from 'node:fs' import { resolve } from 'node:path' @@ -51,51 +50,73 @@ function encodePng(width, height, rgba) { function drawIcon(size) { const buf = Buffer.alloc(size * size * 4) - const c0 = [56, 189, 248] - const c1 = [99, 102, 241] + const paperR = 248, paperG = 250, paperB = 252 // #F8FAFC + const gridR = 185, gridG = 213, gridB = 246 // #B9D5F6 + const redR = 239, redG = 68, redB = 68 // #EF4444 + const lineR = 236, lineG = 73, lineB = 73 // #EC4949 const cx = size / 2 const cy = size / 2 - const rInner = size * 0.3 - const rPad = size * 0.16 + const gridSize = size / 10 for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { const i = (y * size + x) * 4 - const dx = x - cx - const dy = y - cy - const dist = Math.sqrt(dx * dx + dy * dy) - // Fond arrondi : on coupe les coins - let transparent = false - if (x < rPad && y < rPad) transparent = Math.hypot(rPad - x, rPad - y) > rPad - else if (x >= size - rPad && y < rPad) transparent = Math.hypot(x - (size - rPad), rPad - y) > rPad - else if (x < rPad && y >= size - rPad) transparent = Math.hypot(rPad - x, y - (size - rPad)) > rPad - else if (x >= size - rPad && y >= size - rPad) transparent = Math.hypot(x - (size - rPad), y - (size - rPad)) > rPad + // Fond papier + let r = paperR, g = paperG, b = paperB, a = 255 - if (transparent) { - buf[i + 3] = 0 - continue + // Quadrillage + const gx = x % gridSize + const gy = y % gridSize + const nearVx = gx < 1.5 || gx > gridSize - 1.5 + const nearHy = gy < 1.5 || gy > gridSize - 1.5 + if (nearVx || nearHy) { + r = gridR; g = gridG; b = gridB } - const t = (x + y) / (size * 2) - let r = c0[0] + (c1[0] - c0[0]) * t - let g = c0[1] + (c1[1] - c0[1]) * t - let b = c0[2] + (c1[2] - c0[2]) * t - - if (dist < rInner) { - r = 255 - g = 255 - b = 255 - } else if (dist < rInner + size * 0.025) { - r = Math.min(255, r + 50) - g = Math.min(255, g + 50) - b = Math.min(255, b + 50) + // Marge rouge verticale (style cahier) + if (x < size * 0.08 && x > size * 0.05) { + r = lineR; g = lineG; b = lineB } - buf[i] = Math.round(r) - buf[i + 1] = Math.round(g) - buf[i + 2] = Math.round(b) - buf[i + 3] = 255 + // Dessiner le "0" rouge (ellipse inclinée) + const dx = (x - cx) / (size * 0.22) + const dy = (y - cy) / (size * 0.3) + const dist0 = Math.sqrt(dx * dx + dy * dy) + + // Anneau du 0 + if (dist0 > 0.7 && dist0 < 1.15) { + r = redR; g = redG; b = redB + } + + // Rayons du soleil (petits traits autour du 0) + const angle = Math.atan2(y - cy, x - cx) + const rayDist = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2) + const rayOuter = size * 0.42 + const rayInner = size * 0.36 + if (rayDist > rayInner && rayDist < rayOuter) { + // 8 rayons + const rayAngle = (Math.round(angle / (Math.PI / 4)) * Math.PI) / 4 + const perpDist = Math.abs((x - cx) * Math.sin(rayAngle) - (y - cy) * Math.cos(rayAngle)) + if (perpDist < size * 0.02) { + r = redR; g = redG; b = redB + } + } + + // Soulignement double en bas + const underY1 = cy + size * 0.38 + const underY2 = cy + size * 0.42 + if (Math.abs(y - underY1) < size * 0.015 && x > cx - size * 0.3 && x < cx + size * 0.3) { + r = redR; g = redG; b = redB + } + if (Math.abs(y - underY2) < size * 0.015 && x > cx - size * 0.28 && x < cx + size * 0.32) { + r = redR; g = redG; b = redB + } + + buf[i] = r + buf[i + 1] = g + buf[i + 2] = b + buf[i + 3] = a } } return buf diff --git a/shared/src/index.ts b/shared/src/index.ts index f5dde43..22438c1 100644 --- a/shared/src/index.ts +++ b/shared/src/index.ts @@ -1,90 +1,128 @@ /** * 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. + * Charte graphique V2 — 8 états météo. */ -/** Statut météorologique d'une évaluation. */ -export type WeatherKind = 'soleil' | 'brouillard' | 'vent' | 'orage' | 'tempete' +export type WeatherKind = + | 'arcenciel' // aucune évaluation + événement spécial + | 'soleil' // aucune évaluation + | 'brouillard' // suspicion + | 'nuageux' // petite évaluation / QCM + | 'pluie' // évaluation classique + | 'orage' // DS / contrôle surveillé + | 'tempete' // bac blanc / examen blanc + | 'fin_du_monde' // vrai bac -export const WEATHER_KINDS: WeatherKind[] = ['soleil', 'brouillard', 'vent', 'orage', 'tempete'] +export const WEATHER_KINDS: WeatherKind[] = [ + 'arcenciel', 'soleil', 'brouillard', 'nuageux', 'pluie', 'orage', 'tempete', 'fin_du_monde', +] -/** Du plus rassurant au plus menaçant. Sert aux tris et au calcul du statut d'une journée. */ export const SEVERITY: Record = { + arcenciel: 0, soleil: 0, brouillard: 1, - vent: 2, - orage: 3, - tempete: 4, + nuageux: 2, + pluie: 3, + orage: 4, + tempete: 5, + fin_du_monde: 6, } 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. */ + /** Couleur principale du statut (hex). */ 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 + /** Classe de couleur du texte seule. */ + accent: string + /** Gradient CSS pour les cartes hero / mini-arts. */ + gradient: string } export const WEATHER: Record = { + arcenciel: { + label: 'Arc-en-ciel', + title: 'Arc-en-ciel', + icon: '🌈', + hint: 'Aucune évaluation + événement spécial : sortie, pas de cours…', + hex: '#3B82F6', + badge: 'bg-blue-500/15 text-blue-300 border-blue-400/30', + accent: 'text-blue-400', + gradient: 'linear-gradient(145deg,#1570EF,#7555D9)', + }, soleil: { - label: 'Grand Soleil', - title: 'Grand Soleil', + label: 'Ensoleillé', + title: 'Ensoleillé', 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', + hint: 'Aucune évaluation aujourd’hui.', + hex: '#FFD43B', + badge: 'bg-amber-400/15 text-amber-300 border-amber-400/30', + accent: 'text-amber-300', + gradient: 'linear-gradient(145deg,#1488FF,#55B8FF)', }, brouillard: { - label: 'Brouillard', - title: 'Brouillard', + label: 'Brumeux', + title: 'Brumeux', 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', + hint: 'Suspicion d’évaluation aujourd’hui.', + hex: '#9CA3AF', + badge: 'bg-slate-400/15 text-slate-300 border-slate-400/30', + accent: 'text-slate-300', + gradient: 'linear-gradient(145deg,#A6B5C9,#7288A8)', }, - 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', + nuageux: { + label: 'Nuageux', + title: 'Nuageux', + icon: '☁️', + hint: 'Petite évaluation, exposé ou QCM aujourd’hui.', + hex: '#60A5FA', + badge: 'bg-sky-400/15 text-sky-300 border-sky-400/30', + accent: 'text-sky-400', + gradient: 'linear-gradient(145deg,#57B7FF,#2D86ED)', + }, + pluie: { + label: 'Pluvieux', + title: 'Pluvieux', + icon: '🌧️', + hint: 'Évaluation classique aujourd’hui.', + hex: '#3B82F6', + badge: 'bg-blue-500/15 text-blue-300 border-blue-400/30', + accent: 'text-blue-400', + gradient: 'linear-gradient(145deg,#14589B,#092E64)', }, 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', + label: 'Orageux', + title: 'Orageux', + icon: '⛈️', + hint: 'Devoir surveillé aujourd’hui.', + hex: '#8B5CF6', + badge: 'bg-purple-500/15 text-purple-300 border-purple-400/30', + accent: 'text-purple-400', + gradient: 'linear-gradient(145deg,#291B69,#152758)', }, 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', + label: 'Tempête', + title: 'Tempête', + icon: '🌀', + hint: 'Bac blanc / examen blanc.', + hex: '#EF4444', badge: 'bg-red-500/15 text-red-300 border-red-400/30', + accent: 'text-red-400', + gradient: 'radial-gradient(circle at 50% 50%,#FBAE45 0 5%,#F04464 6% 12%,#7E2BD5 13% 21%,#11194B 22% 36%,#F04464 37% 43%,#11194B 44%)', + }, + fin_du_monde: { + label: 'Fin du monde', + title: 'Fin du monde', + icon: '💥', + hint: 'Vrai bac. Niveau maximal.', + hex: '#FF713D', + badge: 'bg-orange-500/15 text-orange-300 border-orange-400/30', + accent: 'text-orange-400', + gradient: 'linear-gradient(#281846,#B33C33 55%,#F59337)', }, } @@ -92,28 +130,24 @@ 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 < 1) return 'nuageux' + if (weight < 2) return 'pluie' if (weight < 4) return 'orage' - return 'tempete' + if (weight < 5) return 'tempete' + return 'fin_du_monde' } -/** 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: 0, label: '0 — rien' }, + { value: 1, label: '1 — QCM / 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) { @@ -131,10 +165,8 @@ export interface Exam { 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 @@ -146,7 +178,6 @@ export interface ClassInfo { id: number name: string level: string | null - /** Message d'annonce du jour (flash info), lu par tous les membres. */ flashMessage: string | null } @@ -155,11 +186,9 @@ export interface Member { classId: number nickname: string role: Role - /** true = créateur de la classe (ne peut pas être rétrogradé). */ isCreator: boolean } -/** Charge utile de `GET /api/me`, renvoyée aussi à la création et à l'adhésion. */ export interface Session { member: Member klass: ClassInfo @@ -169,7 +198,6 @@ export interface ApiError { error: string } -/** Override météo posé par l'admin pour un jour donné (clé locale AAAA-MM-JJ). */ export interface DayWeather { classId: number dayKey: string @@ -177,15 +205,12 @@ export interface DayWeather { updatedAt: string } -/** Flash info : message quotidien rédigé par l'admin. */ export interface FlashInfo { classId: number - /** null = pas de message publié. */ message: string | null updatedAt: string } -/** Payloads émis par le flux SSE. */ export type StreamEvent = | { type: 'exam.created'; exam: Exam } | { type: 'exam.updated'; exam: Exam } @@ -198,5 +223,4 @@ export type StreamEvent = export const CLASS_CODE_LENGTH = 6 -/** Version de l'application. Incrémentée à chaque release APK. */ -export const APP_VERSION = '0.5.0' +export const APP_VERSION = '1.0.0' diff --git a/web/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/web/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index c023e50..01b714f 100644 Binary files a/web/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/web/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/web/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/web/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 72905b8..01b714f 100644 Binary files a/web/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/web/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 4d1e077..01b714f 100644 Binary files a/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/web/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 6cdf97c..01b714f 100644 Binary files a/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/web/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 46de6e2..5f8825c 100644 Binary files a/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/web/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/web/index.html b/web/index.html index 9c39029..16fddb9 100644 --- a/web/index.html +++ b/web/index.html @@ -1,40 +1,29 @@ - + - + Météo Exam - - - - - + - -
+ +