/** * CyberSim OS - Immersive Workstation Login & Network Selection Screen (Phase 2.5) * * Immersive fictional workstation lockscreen, runtime language switching, * learner first & last name validation, and dynamic workplace network selection. */ import { i18n } from './i18n.js'; import { branding } from './branding.js'; export class LoginScreen { constructor({ containerElement, onConnect }) { this.container = containerElement; this.onConnect = onConnect; this.selectedScenarioId = null; this.scenarios = []; this.unsubscribeI18n = null; } /** * Escape HTML to ensure learner input is never rendered as executable markup. */ static escapeHtml(str) { if (typeof str !== 'string') return ''; return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** * Initialize and render the login screen. */ async render() { this.scenarios = branding.getScenariosCatalog(); if (this.scenarios.length > 0 && !this.selectedScenarioId) { this.selectedScenarioId = this.scenarios[0].id; } this._drawUI(); // Listen to language changes to re-render login UI dynamically if (this.unsubscribeI18n) this.unsubscribeI18n(); this.unsubscribeI18n = i18n.onChange(() => { this._drawUI(); }); } _drawUI() { if (!this.container) return; const org = branding.getOrganization(); const enabledLocales = i18n.getEnabledLocales(); const currentLocale = i18n.getLocale(); // Preserve entered first and last name if switching language const existingFirstInput = this.container.querySelector('#login-firstname-input'); const existingFirstName = existingFirstInput ? existingFirstInput.value : ''; const existingLastInput = this.container.querySelector('#login-lastname-input'); const existingLastName = existingLastInput ? existingLastInput.value : ''; this.container.innerHTML = `
`; this._bindEvents(); } _getNetworkIconSvg(type) { switch (type) { case 'health': return ``; case 'corporate': default: return ``; } } _bindEvents() { // Language dropdown change const langSelect = this.container.querySelector('#login-lang-dropdown'); if (langSelect) { langSelect.addEventListener('change', async (e) => { const newLocale = e.target.value; await i18n.setLocale(newLocale); }); } // Network card selection const cards = this.container.querySelectorAll('.cs-network-card'); cards.forEach(card => { card.addEventListener('click', () => { if (card.dataset.supported === 'false') return; cards.forEach(c => { c.classList.remove('selected'); const radio = c.querySelector('input[type="radio"]'); if (radio) radio.checked = false; }); card.classList.add('selected'); const radio = card.querySelector('input[type="radio"]'); if (radio) radio.checked = true; this.selectedScenarioId = card.dataset.scenarioId; }); }); // Inputs on Enter const firstNameInput = this.container.querySelector('#login-firstname-input'); const lastNameInput = this.container.querySelector('#login-lastname-input'); [firstNameInput, lastNameInput].forEach(inp => { if (inp) { inp.addEventListener('keydown', (e) => { if (e.key === 'Enter') { this._handleConnect(); } }); } }); if (firstNameInput && !firstNameInput.value) { firstNameInput.focus(); } else if (lastNameInput && !lastNameInput.value) { lastNameInput.focus(); } // Connect button click const connectBtn = this.container.querySelector('#btn-login-connect'); if (connectBtn) { connectBtn.addEventListener('click', () => this._handleConnect()); } } _showError(msg) { const errorEl = this.container.querySelector('#login-error-msg'); if (errorEl) { errorEl.textContent = msg; errorEl.style.display = 'block'; } } _clearError() { const errorEl = this.container.querySelector('#login-error-msg'); if (errorEl) { errorEl.textContent = ''; errorEl.style.display = 'none'; } } _handleConnect() { this._clearError(); const firstNameInput = this.container.querySelector('#login-firstname-input'); const lastNameInput = this.container.querySelector('#login-lastname-input'); const firstName = firstNameInput ? firstNameInput.value.trim() : ''; const lastName = lastNameInput ? lastNameInput.value.trim() : ''; // 1. Validate First Name if (!firstName) { this._showError(i18n.t('login.firstNameRequired')); if (firstNameInput) firstNameInput.focus(); return; } if (firstName.length > 50) { this._showError(i18n.t('login.firstNameTooLong')); if (firstNameInput) firstNameInput.focus(); return; } // 2. Validate Last Name if (!lastName) { this._showError(i18n.t('login.lastNameRequired')); if (lastNameInput) lastNameInput.focus(); return; } if (lastName.length > 50) { this._showError(i18n.t('login.lastNameTooLong')); if (lastNameInput) lastNameInput.focus(); return; } // 3. Validate Selected Network const selectedScenario = this.scenarios.find(s => s.id === this.selectedScenarioId); if (!selectedScenario) { this._showError(i18n.t('login.selectNetwork')); return; } const currentLocale = i18n.getLocale(); if (selectedScenario.supportedLocales && !selectedScenario.supportedLocales.includes(currentLocale)) { this._showError(i18n.t('login.networkUnavailable')); return; } // Clean up listener if (this.unsubscribeI18n) { this.unsubscribeI18n(); this.unsubscribeI18n = null; } // Connect callback if (this.onConnect) { this.onConnect({ firstName, lastName, name: `${firstName} ${lastName}`, scenarioId: selectedScenario.id, scenarioPath: selectedScenario.path, locale: currentLocale }); } } }