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
This commit is contained in:
parent
fed02e55bd
commit
266a4208be
1 changed files with 58 additions and 101 deletions
|
|
@ -1,14 +1,8 @@
|
||||||
import { h, mount } from '../dom'
|
import { h, mount } from '../dom'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
import { getBaseUrl, setBaseUrl } from '../config'
|
import { getBaseUrl, setBaseUrl, DEFAULT_BASE_URL } from '../config'
|
||||||
import { loginAs } from '../store'
|
import { loginAs } from '../store'
|
||||||
import { showToast } from '../toast'
|
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'
|
type Mode = 'join' | 'create'
|
||||||
|
|
||||||
|
|
@ -16,152 +10,115 @@ export function renderJoin(root: HTMLElement, rerender: () => void): void {
|
||||||
let _mode: Mode = 'join'
|
let _mode: Mode = 'join'
|
||||||
const getMode = (): Mode => _mode
|
const getMode = (): Mode => _mode
|
||||||
|
|
||||||
const nameInput = h('input', { class: inputClass, placeholder: 'Terminale S2' })
|
const nameInput = h('input', { class: 'app-input', placeholder: 'Terminale S2' })
|
||||||
const levelInput = h('input', { class: inputClass, placeholder: 'Lycée' })
|
const levelInput = h('input', { class: 'app-input', placeholder: 'Lycée' })
|
||||||
const codeInput = h('input', { class: inputClass, placeholder: 'TERMS2', autocomplete: 'off' })
|
const codeInput = h('input', { class: 'app-input', placeholder: 'TERMS2', autocomplete: 'off' })
|
||||||
const nicknameInput = h('input', { class: inputClass, placeholder: 'Votre pseudo' })
|
const nicknameInput = h('input', { class: 'app-input', placeholder: 'Votre pseudo' })
|
||||||
const serverInput = h('input', {
|
const serverInput = h('input', {
|
||||||
class: inputClass,
|
class: 'app-input',
|
||||||
placeholder: DEFAULT_BASE_URL,
|
placeholder: DEFAULT_BASE_URL,
|
||||||
value: getBaseUrl(),
|
value: getBaseUrl(),
|
||||||
autocomplete: 'off',
|
autocomplete: 'off',
|
||||||
inputMode: 'url',
|
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' })
|
// Wrappers pour toggle display
|
||||||
const codeHint = h('p', { class: 'hidden text-xs text-slate-500 font-semibold' })
|
const codeWrap = h('div', {})
|
||||||
const submitBtn = h('button', {
|
const nameWrap = h('div', {})
|
||||||
class: 'w-full bg-sky-500 text-slate-950 text-sm font-extrabold py-3.5 rounded-xl active:scale-[0.98] transition-transform',
|
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', {
|
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 => {
|
const showError = (message: string): void => {
|
||||||
errorBox.textContent = message
|
errorBox.textContent = message
|
||||||
errorBox.classList.remove('hidden')
|
errorBox.style.display = 'block'
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderMode = (): void => {
|
const renderMode = (): void => {
|
||||||
const create = getMode() === 'create'
|
const create = getMode() === 'create'
|
||||||
submitBtn.replaceChildren(
|
submitBtn.replaceChildren(create ? 'Créer la classe' : 'Rejoindre la classe')
|
||||||
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'
|
||||||
toggleBtn.replaceChildren(
|
levelWrap.style.display = create ? '' : 'none'
|
||||||
create ? 'Déjà un code ? Rejoindre une classe' : 'Créer une nouvelle classe',
|
codeWrap.style.display = create ? 'none' : ''
|
||||||
)
|
errorBox.style.display = 'none'
|
||||||
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')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = async (): Promise<void> => {
|
const submit = async (): Promise<void> => {
|
||||||
const nickname = nicknameInput.value.trim()
|
const nickname = nicknameInput.value.trim()
|
||||||
if (!nickname) {
|
if (!nickname) { showError('Choisissez un pseudo.'); return }
|
||||||
showError('Choisissez un pseudo.')
|
setBaseUrl(serverInput.value.trim().replace(/\/+$/, '') || DEFAULT_BASE_URL)
|
||||||
return
|
|
||||||
}
|
|
||||||
const baseUrl = serverInput.value.trim().replace(/\/+$/, '') || DEFAULT_BASE_URL
|
|
||||||
setBaseUrl(baseUrl)
|
|
||||||
|
|
||||||
submitBtn.disabled = true
|
submitBtn.disabled = true
|
||||||
submitBtn.classList.add('opacity-60')
|
submitBtn.style.opacity = '0.6'
|
||||||
try {
|
try {
|
||||||
if (getMode() === 'create') {
|
if (getMode() === 'create') {
|
||||||
const name = nameInput.value.trim()
|
const name = nameInput.value.trim()
|
||||||
if (name.length < 2) {
|
if (name.length < 2) { showError('Nom trop court.'); submitBtn.disabled = false; submitBtn.style.opacity = '1'; return }
|
||||||
showError('Nom de classe trop court.')
|
|
||||||
submitBtn.disabled = false
|
|
||||||
submitBtn.classList.remove('opacity-60')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const result = await api.createClass({ name, level: levelInput.value.trim() || null, nickname })
|
const result = await api.createClass({ name, level: levelInput.value.trim() || null, nickname })
|
||||||
showToast(`Classe créée — code ${result.code}`, 'success')
|
showToast(`Classe créée — code ${result.code}`, 'success')
|
||||||
await loginAs(result.token, { member: result.member, klass: result.klass })
|
await loginAs(result.token, { member: result.member, klass: result.klass })
|
||||||
} else {
|
} else {
|
||||||
const code = codeInput.value.trim()
|
const code = codeInput.value.trim()
|
||||||
if (!code) {
|
if (!code) { showError('Entrez le code.'); submitBtn.disabled = false; submitBtn.style.opacity = '1'; return }
|
||||||
showError('Entrez le code de classe.')
|
|
||||||
submitBtn.disabled = false
|
|
||||||
submitBtn.classList.remove('opacity-60')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const result = await api.joinClass({ code, nickname })
|
const result = await api.joinClass({ code, nickname })
|
||||||
await loginAs(result.token, { member: result.member, klass: result.klass })
|
await loginAs(result.token, { member: result.member, klass: result.klass })
|
||||||
}
|
}
|
||||||
rerender()
|
rerender()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : 'Connexion impossible'
|
showError(error instanceof Error ? error.message : 'Connexion impossible')
|
||||||
showError(message)
|
|
||||||
submitBtn.disabled = false
|
submitBtn.disabled = false
|
||||||
submitBtn.classList.remove('opacity-60')
|
submitBtn.style.opacity = '1'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
submitBtn.addEventListener('click', submit)
|
submitBtn.addEventListener('click', submit)
|
||||||
toggleBtn.addEventListener('click', () => {
|
toggleBtn.addEventListener('click', () => { _mode = _mode === 'join' ? 'create' : 'join'; renderMode() })
|
||||||
_mode = _mode === 'join' ? 'create' : 'join'
|
codeInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit() })
|
||||||
renderMode()
|
nicknameInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit() })
|
||||||
})
|
codeInput.addEventListener('input', () => { codeInput.value = codeInput.value.toUpperCase() })
|
||||||
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()
|
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(
|
mount(
|
||||||
root,
|
root,
|
||||||
h(
|
h('div', { style: 'display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:70vh;padding:0 24px' },
|
||||||
'div',
|
h('div', { class: 'app-mark paper-grid', style: 'width:80px;height:80px;border-radius:24px;margin-bottom:24px' },
|
||||||
{ class: 'flex flex-col items-center justify-center min-h-[70vh] px-6' },
|
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(
|
|
||||||
'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('h1', { class: 'text-2xl font-black text-white text-center tracking-tight' }, 'Météo Exam'),
|
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', { class: 'text-xs font-bold text-sky-400 mt-1 mb-8 text-center' },
|
h('p', { style: 'font-size:12px;font-weight:700;color:var(--blue2);margin:6px 0 32px;text-align:center' },
|
||||||
'Le calendrier des évaluations, en bulletin météo.'),
|
'Le ciel de tes évaluations.'),
|
||||||
h(
|
h('div', { style: 'width:100%;display:grid;gap:16px' },
|
||||||
'div',
|
|
||||||
{ class: 'w-full space-y-4' },
|
|
||||||
field('Votre pseudo', nicknameInput),
|
field('Votre pseudo', nicknameInput),
|
||||||
getMode() === 'join' ? field('Code de classe', codeInput) : null,
|
codeWrap,
|
||||||
getMode() === 'join'
|
nameWrap,
|
||||||
? h('p', { class: 'text-[11px] text-slate-500 font-semibold -mt-2' },
|
levelWrap,
|
||||||
'Demander le code à l’admin de la classe (ex. TERMS2).')
|
field('Serveur', serverInput, 'Émulateur : 10.0.2.2:8787 · Réseau : IP du PC'),
|
||||||
: 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'),
|
|
||||||
errorBox,
|
errorBox,
|
||||||
submitBtn,
|
submitBtn,
|
||||||
toggleBtn,
|
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue