meteo-exam/web/src/views/join.ts
Mathis 266a4208be fix : join screen redesign V2 (papier quadrillé + 0 rouge, Fredoka), fix crash parentElement null
- Join screen utilise le design V2 (app-mark paper-grid, Fredoka, palette V2)
- Fix : parentElement null avant mount → wrappers séparés pour toggle join/create
- try-catch sur le rendu des vues pour capturer les erreurs silencieuses
2026-10-09 21:51:29 +02:00

124 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { h, mount } from '../dom'
import { api } from '../api'
import { getBaseUrl, setBaseUrl, DEFAULT_BASE_URL } from '../config'
import { loginAs } from '../store'
import { showToast } from '../toast'
type Mode = 'join' | 'create'
export function renderJoin(root: HTMLElement, rerender: () => void): void {
let _mode: Mode = 'join'
const getMode = (): Mode => _mode
const nameInput = h('input', { class: 'app-input', placeholder: 'Terminale S2' })
const levelInput = h('input', { class: 'app-input', placeholder: 'Lycée' })
const codeInput = h('input', { class: 'app-input', placeholder: 'TERMS2', autocomplete: 'off' })
const nicknameInput = h('input', { class: 'app-input', placeholder: 'Votre pseudo' })
const serverInput = h('input', {
class: 'app-input',
placeholder: DEFAULT_BASE_URL,
value: getBaseUrl(),
autocomplete: 'off',
inputMode: 'url',
})
// Wrappers pour toggle display
const codeWrap = h('div', {})
const nameWrap = h('div', {})
const levelWrap = h('div', {})
const errorBox = h('p', {
style: 'display:none;font-size:12px;font-weight:700;color:#FB7185;background:color-mix(in srgb,var(--red) 10%,transparent);border:1px solid color-mix(in srgb,var(--red) 25%,transparent);border-radius:10px;padding:8px 12px',
})
const submitBtn = h('button', { class: 'btn-primary', style: 'width:100%' })
const toggleBtn = h('button', {
style: 'width:100%;font-size:12px;font-weight:900;color:var(--blue2);padding:10px;background:none;border:0;cursor:pointer',
})
const showError = (message: string): void => {
errorBox.textContent = message
errorBox.style.display = 'block'
}
const renderMode = (): void => {
const create = getMode() === 'create'
submitBtn.replaceChildren(create ? 'Créer la classe' : 'Rejoindre la classe')
toggleBtn.replaceChildren(create ? 'Déjà un code ? Rejoindre' : 'Créer une nouvelle classe')
nameWrap.style.display = create ? '' : 'none'
levelWrap.style.display = create ? '' : 'none'
codeWrap.style.display = create ? 'none' : ''
errorBox.style.display = 'none'
}
const submit = async (): Promise<void> => {
const nickname = nicknameInput.value.trim()
if (!nickname) { showError('Choisissez un pseudo.'); return }
setBaseUrl(serverInput.value.trim().replace(/\/+$/, '') || DEFAULT_BASE_URL)
submitBtn.disabled = true
submitBtn.style.opacity = '0.6'
try {
if (getMode() === 'create') {
const name = nameInput.value.trim()
if (name.length < 2) { showError('Nom trop court.'); submitBtn.disabled = false; submitBtn.style.opacity = '1'; return }
const result = await api.createClass({ name, level: levelInput.value.trim() || null, nickname })
showToast(`Classe créée — code ${result.code}`, 'success')
await loginAs(result.token, { member: result.member, klass: result.klass })
} else {
const code = codeInput.value.trim()
if (!code) { showError('Entrez le code.'); submitBtn.disabled = false; submitBtn.style.opacity = '1'; return }
const result = await api.joinClass({ code, nickname })
await loginAs(result.token, { member: result.member, klass: result.klass })
}
rerender()
} catch (error) {
showError(error instanceof Error ? error.message : 'Connexion impossible')
submitBtn.disabled = false
submitBtn.style.opacity = '1'
}
}
submitBtn.addEventListener('click', submit)
toggleBtn.addEventListener('click', () => { _mode = _mode === 'join' ? 'create' : 'join'; renderMode() })
codeInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit() })
nicknameInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit() })
codeInput.addEventListener('input', () => { codeInput.value = codeInput.value.toUpperCase() })
renderMode()
const field = (label: string, control: HTMLElement, hint?: string): HTMLElement =>
h('div', {},
h('span', { class: 'app-label' }, label),
control,
hint ? h('span', { style: 'display:block;font-size:10px;color:var(--muted);font-weight:700;margin-top:4px' }, hint) : null,
)
// Remplir les wrappers
codeWrap.append(field('Code de classe', codeInput, 'Demander le code à l’admin.'))
nameWrap.append(field('Nom de la classe', nameInput))
levelWrap.append(field('Niveau (optionnel)', levelInput))
mount(
root,
h('div', { style: 'display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:70vh;padding:0 24px' },
h('div', { class: 'app-mark paper-grid', style: 'width:80px;height:80px;border-radius:24px;margin-bottom:24px' },
h('div', { style: 'font-family:\"Comic Sans MS\",\"Bradley Hand\",cursive;color:#EF4444;font-size:60px;font-weight:900;font-style:italic;line-height:80px;text-align:center;transform:rotate(-8deg)' }, '0'),
),
h('h1', { style: 'font-size:24px;font-weight:700;color:var(--text);text-align:center;font-family:Fredoka,sans-serif' }, 'Météo Exam'),
h('p', { style: 'font-size:12px;font-weight:700;color:var(--blue2);margin:6px 0 32px;text-align:center' },
'Le ciel de tes évaluations.'),
h('div', { style: 'width:100%;display:grid;gap:16px' },
field('Votre pseudo', nicknameInput),
codeWrap,
nameWrap,
levelWrap,
field('Serveur', serverInput, 'Émulateur : 10.0.2.2:8787 · Réseau : IP du PC'),
errorBox,
submitBtn,
toggleBtn,
),
h('p', { style: 'font-size:10px;color:var(--muted);font-weight:800;margin-top:32px;text-align:center' },
'L’admin crée le calendrier. Les élèves le consultent.'),
),
)
}