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:
commit
6d154ed5fb
92 changed files with 9574 additions and 0 deletions
152
web/src/views/map.ts
Normal file
152
web/src/views/map.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import { WEATHER } from '@meteo-exam/shared'
|
||||
import { h, mount } from '../dom'
|
||||
|
||||
const SVG_NS = 'http://www.w3.org/2000/svg'
|
||||
|
||||
function svgEl<K extends keyof SVGElementTagNameMap>(
|
||||
tag: K,
|
||||
attrs: Record<string, string | number> = {},
|
||||
): SVGElementTagNameMap[K] {
|
||||
const el = document.createElementNS(SVG_NS, tag)
|
||||
for (const [key, value] of Object.entries(attrs)) el.setAttribute(key, String(value))
|
||||
return el
|
||||
}
|
||||
|
||||
interface Region {
|
||||
id: string
|
||||
subject: string
|
||||
kind: keyof typeof WEATHER
|
||||
weight: number
|
||||
desc: string
|
||||
d: string
|
||||
labelX: number
|
||||
labelY: number
|
||||
emojiX: number
|
||||
emojiY: number
|
||||
}
|
||||
|
||||
const REGIONS: Region[] = [
|
||||
{ id: 'maths', subject: 'Mathématiques', kind: 'tempete', weight: 4, d: 'M 150,40 Q 250,20 350,50 L 320,160 Q 220,180 160,150 Z', labelX: 230, labelY: 120, emojiX: 230, emojiY: 90, desc: 'Avis de forte tempête. Contrôle continu à fort coefficient programmé.' },
|
||||
{ id: 'svt', subject: 'SVT', kind: 'vent', weight: 2, d: 'M 350,50 Q 460,80 440,200 L 310,220 Q 300,160 320,160 Z', labelX: 375, labelY: 160, emojiX: 375, emojiY: 130, desc: 'Risque de petit QCM surprise en début d’heure sur le chapitre de la géologie.' },
|
||||
{ id: 'physique', subject: 'Physique-Chimie', kind: 'brouillard', weight: 2, d: 'M 160,150 Q 220,180 310,220 L 280,320 Q 180,310 140,240 Z', labelX: 220, labelY: 255, emojiX: 220, emojiY: 225, desc: 'Suspicion de TP évalué la semaine prochaine. Rumeurs soutenues par la classe.' },
|
||||
{ id: 'histoire', subject: 'Histoire-Géo', kind: 'soleil', weight: 1, d: 'M 150,40 L 160,150 Q 140,240 60,220 Q 30,120 150,40 Z', labelX: 90, labelY: 160, emojiX: 90, emojiY: 130, desc: 'Conditions idéales. Aucun contrôle prévu d’ici la fin de la semaine.' },
|
||||
{ id: 'francais', subject: 'Français', kind: 'soleil', weight: 1, d: 'M 140,240 Q 180,310 280,320 L 260,380 Q 120,390 60,220 Z', labelX: 170, labelY: 340, emojiX: 170, emojiY: 315, desc: 'Aucune perturbation à signaler. Cours magistral standard.' },
|
||||
]
|
||||
|
||||
/** Carte SVG par matière — figée, alimentée par les données en phase 2. */
|
||||
export function renderMap(root: HTMLElement, _rerender: () => void): void {
|
||||
let selected = 'maths'
|
||||
|
||||
const detailTitle = h('h3', { class: 'text-base font-black text-slate-100' })
|
||||
const detailStatus = h('p', { class: 'text-xs font-bold' })
|
||||
const detailCoeff = h('span', { class: 'bg-slate-700 text-slate-200 text-xs font-black px-2.5 py-1 rounded-lg' })
|
||||
const detailDesc = h('p', { class: 'text-xs text-slate-300 font-medium leading-relaxed mt-2 pt-2 border-t border-slate-700/60' })
|
||||
const detailIcon = h('span', { class: 'text-3xl' })
|
||||
|
||||
const statusClasses: Record<string, string> = {
|
||||
tempete: 'text-rose-400',
|
||||
orage: 'text-rose-400',
|
||||
vent: 'text-amber-400',
|
||||
brouillard: 'text-purple-400',
|
||||
soleil: 'text-emerald-400',
|
||||
}
|
||||
|
||||
const updateDetail = (region: Region): void => {
|
||||
const meta = WEATHER[region.kind]
|
||||
detailIcon.textContent = meta.icon
|
||||
detailTitle.textContent = `Secteur : ${region.subject}`
|
||||
detailStatus.textContent = `${meta.title} • ${meta.hint}`
|
||||
detailStatus.className = `text-xs font-bold ${statusClasses[region.kind] ?? 'text-slate-400'}`
|
||||
detailCoeff.textContent = region.weight > 0 ? `Coeff ${region.weight.toFixed(1)}` : ' Calme'
|
||||
detailDesc.textContent = region.desc
|
||||
}
|
||||
|
||||
const paths = new Map<string, SVGPathElement>()
|
||||
const svg = svgEl('svg', { viewBox: '0 0 500 400' })
|
||||
svg.setAttribute('class', 'w-full h-full drop-shadow-md')
|
||||
|
||||
for (const region of REGIONS) {
|
||||
const meta = WEATHER[region.kind]
|
||||
const path = svgEl('path', {
|
||||
id: `region-${region.id}`,
|
||||
d: region.d,
|
||||
fill: meta.hex,
|
||||
})
|
||||
path.setAttribute('class', 'map-region')
|
||||
path.setAttribute('style', 'transition:all 0.3s cubic-bezier(0.4,0,0.2,1);cursor:pointer;stroke:#0f172a;stroke-width:2.5')
|
||||
|
||||
path.addEventListener('click', () => {
|
||||
selected = region.id
|
||||
for (const p of paths.values()) p.classList.remove('active-region')
|
||||
path.classList.add('active-region')
|
||||
updateDetail(region)
|
||||
})
|
||||
|
||||
paths.set(region.id, path)
|
||||
svg.append(path)
|
||||
|
||||
const emoji = svgEl('text', {
|
||||
x: region.emojiX,
|
||||
y: region.emojiY,
|
||||
'font-size': '22',
|
||||
'pointer-events': 'none',
|
||||
})
|
||||
emoji.textContent = meta.icon
|
||||
svg.append(emoji)
|
||||
|
||||
const label = svgEl('text', {
|
||||
x: region.labelX,
|
||||
y: region.labelY,
|
||||
'font-size': '12',
|
||||
'font-weight': '900',
|
||||
fill: '#ffffff',
|
||||
'text-anchor': 'middle',
|
||||
'pointer-events': 'none',
|
||||
})
|
||||
label.textContent = region.subject
|
||||
svg.append(label)
|
||||
}
|
||||
|
||||
paths.get(selected)?.classList.add('active-region')
|
||||
updateDetail(REGIONS[0]!)
|
||||
|
||||
mount(
|
||||
root,
|
||||
h(
|
||||
'div',
|
||||
{ class: 'space-y-4' },
|
||||
h(
|
||||
'div',
|
||||
{ class: 'flex justify-between items-center' },
|
||||
h('div', {},
|
||||
h('h2', { class: 'text-lg font-black text-slate-100' }, 'Carte Météorologique'),
|
||||
h('p', { class: 'text-xs font-semibold text-slate-400' }, 'Secteurs géographiques par matière'),
|
||||
),
|
||||
h('span', { class: 'px-2.5 py-1 rounded-full bg-sky-500/10 text-sky-400 text-xs font-bold border border-sky-500/20' },
|
||||
'Interactive'),
|
||||
),
|
||||
h(
|
||||
'div',
|
||||
{ class: 'bg-slate-900 border border-slate-800 rounded-3xl p-3 shadow-lg relative overflow-hidden aspect-[4/3] flex items-center justify-center' },
|
||||
svg,
|
||||
),
|
||||
h(
|
||||
'div',
|
||||
{ class: 'bg-slate-800 border border-slate-700 rounded-2xl p-4' },
|
||||
h('div', { class: 'flex justify-between items-center mb-2' },
|
||||
h('div', { class: 'flex items-center gap-2.5' },
|
||||
detailIcon,
|
||||
h('div', {}, detailTitle, detailStatus),
|
||||
),
|
||||
detailCoeff,
|
||||
),
|
||||
detailDesc,
|
||||
),
|
||||
h(
|
||||
'p',
|
||||
{ class: 'text-[10px] text-slate-600 font-semibold text-center' },
|
||||
'Les secteurs seront alimentés par les données de la classe en phase 2.',
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue