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

125 lines
5.6 KiB
TypeScript
Raw Normal View History

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