import './style.css' import { el, svuota } from './components/dom' import { vistaLanding } from './views/landing' import { vistaHome } from './views/home' import { vistaModulo } from './views/modulo' import { vistaDrill } from './views/drill' import { vistaPiramide } from './views/piramide' import { vistaTocca5 } from './views/tocca5' import { vistaShowdown } from './views/showdown' import { vistaRuoli } from './views/ruoli' import { vistaChips } from './views/chips' import { vistaTimeline } from './views/timeline' import { vistaAnnaBruno } from './views/annabruno' import { vistaDiploma } from './views/diploma' import { vistaPosizionamento } from './views/posizionamento' import { vistaRecupero } from './views/recupero' import { CHIAVE_RITORNO, vistaUnita } from './views/lezione' import { vistaTestLivello } from './views/testLivello' import { vistaGlossario } from './views/glossario' import { vistaImpostazioni } from './views/impostazioni' import { barraUnita } from './components/barraUnita' import { caricaProgresso, moduloSbloccato } from './quiz/storage' import { CHIAVE_AVVISO } from './quiz/avvisi' import { applicaImpostazioni, caricaImpostazioni } from './quiz/impostazioni' import type { Modulo } from './data/tipi' import chevronLeft from './assets/chevron-left.svg' // Mazzo scelto dall'utente applicato prima del primo render. applicaImpostazioni(caricaImpostazioni()) const app = document.querySelector('#app')! /** Un tentativo in corso è l'unico stato che rende l'uscita distruttiva. */ let tentativoInCorso = false function naviga(rotta: string): void { if (tentativoInCorso && !confirm('Il tentativo in corso andrà perso. Uscire comunque?')) return tentativoInCorso = false if (location.hash === rotta) render() else location.hash = rotta } /** Rotte dell'app: rese dentro la cornice (testata + contenitore). */ const ROTTE: Record void) => HTMLElement> = { '#/percorso': vistaHome, '#/trainer/piramide': vistaPiramide, '#/trainer/tocca5': vistaTocca5, '#/trainer/showdown': vistaShowdown, '#/trainer/ruoli': vistaRuoli, '#/trainer/chips': vistaChips, '#/trainer/timeline': vistaTimeline, '#/mano/annabruno': vistaAnnaBruno, '#/mano/diploma': vistaDiploma, '#/trainer/glossario': vistaGlossario, '#/posizionamento': vistaPosizionamento, '#/recupero': vistaRecupero, '#/test-livello': vistaTestLivello, '#/impostazioni': vistaImpostazioni, } /** Hub, unità (con schermata opzionale) e drill di un modulo. */ const ROTTA_MODULO = /^#\/modulo\/(B[1-6])(?:\/(drill)|\/(U\d+)(?:\/([\w.]+))?)?$/ // Rotte storiche: restano per sempre (bookmark, telemetria esportata). const VECCHIA_LEZIONE = /^#\/lezione\/(B[1-6])(?:\/(U\d+))?$/ const VECCHIO_DRILL = /^#\/drill\/(B[1-6])$/ function leggiRitorno(): string | null { try { return sessionStorage.getItem(CHIAVE_RITORNO) } catch { return null } } function scordaRitorno(): void { try { sessionStorage.removeItem(CHIAVE_RITORNO) } catch { // Storage negato: niente da dimenticare. } } /** Testata dell'app: marchio (non è il titolo di pagina), ritorno alla lezione, impostazioni. */ function cornice(ritorno: string | null): HTMLElement { const marchio = el( 'button', { type: 'button', class: 'marchio' }, 'ALL-IN ', el('span', {}, 'ACADEMY'), ) marchio.addEventListener('click', () => naviga('#/')) const testata = el('header', { class: 'testata-app' }, el('p', { class: 'titolo-app' }, marchio)) if (ritorno) { const torna = el( 'button', { type: 'button', class: 'discreto barra-ritorno' }, el('img', { src: chevronLeft, alt: '', width: '14', height: '14', 'aria-hidden': 'true' }), 'Torna alla lezione', ) torna.addEventListener('click', () => { scordaRitorno() naviga(ritorno) }) testata.appendChild(torna) } const impostazioni = el( 'button', { type: 'button', class: 'discreto apri-impostazioni', 'aria-label': 'Impostazioni aspetto', title: 'Aspetto', }, '⚙', ) impostazioni.addEventListener('click', () => naviga('#/impostazioni')) testata.appendChild(impostazioni) return testata } /** Guscio delle stanze del modulo: barra unità + briciole + contenuto. */ function gusciDrill(modulo: Modulo): HTMLElement { const barra = barraUnita({ modulo, suDrill: true, bersaglioSalta: '#contenuto-modulo' }) const briciole = el( 'nav', { class: 'hub-briciole', 'aria-label': 'Percorso' }, el('a', { href: `#/modulo/${modulo}` }, `Modulo ${modulo}`), ) briciole.querySelector('a')!.addEventListener('click', (evento) => { evento.preventDefault() naviga(`#/modulo/${modulo}`) }) const corpo = el( 'div', { id: 'contenuto-modulo', tabindex: '-1' }, vistaDrill(modulo, naviga, { onStato: (inCorso) => { tentativoInCorso = inCorso barra.blocca(inCorso) }, }), ) return el('div', { class: 'guscio-modulo' }, barra.elemento, briciole, corpo) } function render(): void { const grezzo = location.hash // Ancore in-pagina (skip link, "torna al menu"): non sono rotte, il // browser scorre da solo e la vista non va ricostruita. if (grezzo && !grezzo.startsWith('#/') && app.childElementCount > 0) return // Redirezioni permanenti delle rotte storiche, prima di ogni altro match: // `replace` non lascia voci di cronologia, così "indietro" non rimbalza. const vecchiaLezione = VECCHIA_LEZIONE.exec(grezzo) if (vecchiaLezione) { location.replace( `#/modulo/${vecchiaLezione[1]}${vecchiaLezione[2] ? `/${vecchiaLezione[2]}` : ''}`, ) return } const vecchioDrill = VECCHIO_DRILL.exec(grezzo) if (vecchioDrill) { location.replace(`#/modulo/${vecchioDrill[1]}/drill`) return } const hash = grezzo.startsWith('#/') ? grezzo : '#/' // La homepage è la landing: occupa la pagina intera, senza cornice. if (hash === '#/') { svuota(app) app.appendChild(vistaLanding(naviga)) window.scrollTo(0, 0) return } const modulo = ROTTA_MODULO.exec(hash) if (modulo) { const codice = modulo[1] as Modulo // Il gating vale anche per i deep link: URL e schermo restano coerenti // (refresh e back funzionano) e l'avviso spiega il perché. if (!moduloSbloccato(caricaProgresso(), codice)) { try { sessionStorage.setItem(CHIAVE_AVVISO, codice) } catch { // Storage negato: resta la redirezione, senza spiegazione. } location.replace('#/percorso') return } // Dentro il modulo il ritorno alla lezione non ha senso: si è già lì. scordaRitorno() svuota(app) const unitaId = modulo[3] const vista = modulo[2] ? gusciDrill(codice) : unitaId ? vistaUnita(codice, unitaId, modulo[4], naviga) : vistaModulo(codice, naviga) app.appendChild(el('div', { class: 'app-chrome rotta-modulo' }, cornice(null), vista)) // Il player porta il focus sul titolo della schermata: nessuno scroll d'ufficio. if (!unitaId) window.scrollTo(0, 0) return } svuota(app) const vista = (ROTTE[hash] ?? vistaHome)(naviga) app.appendChild(el('div', { class: 'app-chrome' }, cornice(leggiRitorno()), vista)) window.scrollTo(0, 0) } window.addEventListener('hashchange', render) render()