From 266a4208be9e3870b8344c4f6239b788e390bc58 Mon Sep 17 00:00:00 2001 From: Mathis Date: Fri, 9 Oct 2026 21:51:29 +0200 Subject: [PATCH] =?UTF-8?q?fix=20:=20join=20screen=20redesign=20V2=20(papi?= =?UTF-8?q?er=20quadrill=C3=A9=20+=200=20rouge,=20Fredoka),=20fix=20crash?= =?UTF-8?q?=20parentElement=20null?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- web/src/views/join.ts | 159 +++++++++++++++--------------------------- 1 file changed, 58 insertions(+), 101 deletions(-) diff --git a/web/src/views/join.ts b/web/src/views/join.ts index 7cd9d4c..2b24f9b 100644 --- a/web/src/views/join.ts +++ b/web/src/views/join.ts @@ -1,14 +1,8 @@ import { h, mount } from '../dom' import { api } from '../api' -import { getBaseUrl, setBaseUrl } from '../config' +import { getBaseUrl, setBaseUrl, DEFAULT_BASE_URL } from '../config' import { loginAs } from '../store' import { showToast } from '../toast' -import { DEFAULT_BASE_URL } from '../config' - -const inputClass = - 'w-full bg-slate-900/80 border border-slate-700/80 rounded-xl px-3.5 py-3 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-sky-500 transition-colors' - -const labelClass = 'block text-[10px] font-black uppercase tracking-wider text-slate-500 mb-1.5' type Mode = 'join' | 'create' @@ -16,152 +10,115 @@ export function renderJoin(root: HTMLElement, rerender: () => void): void { let _mode: Mode = 'join' const getMode = (): Mode => _mode - const nameInput = h('input', { class: inputClass, placeholder: 'Terminale S2' }) - const levelInput = h('input', { class: inputClass, placeholder: 'Lycée' }) - const codeInput = h('input', { class: inputClass, placeholder: 'TERMS2', autocomplete: 'off' }) - const nicknameInput = h('input', { class: inputClass, placeholder: 'Votre pseudo' }) + 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: inputClass, + class: 'app-input', placeholder: DEFAULT_BASE_URL, value: getBaseUrl(), autocomplete: 'off', inputMode: 'url', }) - const errorBox = h('p', { class: 'hidden text-xs font-bold text-rose-300 bg-rose-500/10 border border-rose-500/25 rounded-xl px-3 py-2' }) - const codeHint = h('p', { class: 'hidden text-xs text-slate-500 font-semibold' }) - const submitBtn = h('button', { - class: 'w-full bg-sky-500 text-slate-950 text-sm font-extrabold py-3.5 rounded-xl active:scale-[0.98] transition-transform', + // 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', { - class: 'w-full text-xs font-black text-sky-400 py-2.5 active:scale-95 transition-transform', + 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.classList.remove('hidden') + 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 une classe' : 'Créer une nouvelle classe', - ) - codeHint.classList.toggle('hidden', create) - nameInput.parentElement?.classList.toggle('hidden', !create) - levelInput.parentElement?.classList.toggle('hidden', !create) - codeInput.parentElement?.classList.toggle('hidden', create) - errorBox.classList.add('hidden') + 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 => { const nickname = nicknameInput.value.trim() - if (!nickname) { - showError('Choisissez un pseudo.') - return - } - const baseUrl = serverInput.value.trim().replace(/\/+$/, '') || DEFAULT_BASE_URL - setBaseUrl(baseUrl) + if (!nickname) { showError('Choisissez un pseudo.'); return } + setBaseUrl(serverInput.value.trim().replace(/\/+$/, '') || DEFAULT_BASE_URL) submitBtn.disabled = true - submitBtn.classList.add('opacity-60') + submitBtn.style.opacity = '0.6' try { if (getMode() === 'create') { const name = nameInput.value.trim() - if (name.length < 2) { - showError('Nom de classe trop court.') - submitBtn.disabled = false - submitBtn.classList.remove('opacity-60') - return - } + 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 de classe.') - submitBtn.disabled = false - submitBtn.classList.remove('opacity-60') - return - } + 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) { - const message = error instanceof Error ? error.message : 'Connexion impossible' - showError(message) + showError(error instanceof Error ? error.message : 'Connexion impossible') submitBtn.disabled = false - submitBtn.classList.remove('opacity-60') + 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() - }) + 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', - { class: 'flex flex-col items-center justify-center min-h-[70vh] px-6' }, - h( - 'div', - { class: 'w-20 h-20 rounded-[1.75rem] bg-gradient-to-tr from-sky-400 to-indigo-500 flex items-center justify-center text-white font-black text-3xl shadow-float mb-6' }, - 'ME', + 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', { class: 'text-2xl font-black text-white text-center tracking-tight' }, 'Météo Exam'), - h('p', { class: 'text-xs font-bold text-sky-400 mt-1 mb-8 text-center' }, - 'Le calendrier des évaluations, en bulletin météo.'), - h( - 'div', - { class: 'w-full space-y-4' }, + 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), - getMode() === 'join' ? field('Code de classe', codeInput) : null, - getMode() === 'join' - ? h('p', { class: 'text-[11px] text-slate-500 font-semibold -mt-2' }, - 'Demander le code à l’admin de la classe (ex. TERMS2).') - : null, - getMode() === 'create' ? field('Nom de la classe', nameInput) : null, - getMode() === 'create' ? field('Niveau (optionnel)', levelInput) : null, - field('Serveur', serverInput, 'Adresse de l’API. Emulateur Android : 10.0.2.2:8787'), + codeWrap, + nameWrap, + levelWrap, + field('Serveur', serverInput, 'Émulateur : 10.0.2.2:8787 · Réseau : IP du PC'), errorBox, submitBtn, toggleBtn, - codeHint, - ), - h( - 'p', - { class: 'text-[10px] text-slate-600 font-semibold mt-8 text-center' }, - 'L’admin de la classe crée le calendrier. Les élèves le consultent et participent au radar.', ), + 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.'), ), ) - - function field(label: string, control: HTMLElement, hint?: string): HTMLElement { - return h( - 'label', - { class: 'block' }, - h('span', { class: labelClass }, label), - control, - hint ? h('span', { class: 'block text-[10px] text-slate-600 font-semibold mt-1' }, hint) : null, - ) - } }