meteo-exam/web/src/views/map.ts

153 lines
5.9 KiB
TypeScript
Raw Normal View History

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.',
),
),
)
}