- 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)
152 lines
5.9 KiB
TypeScript
152 lines
5.9 KiB
TypeScript
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.',
|
||
),
|
||
),
|
||
)
|
||
}
|