Fix translation fallbacks, start menu lifecycle, add user last name, and verifier certificate print

This commit is contained in:
2026-08-24 08:52:50 -07:00
parent 7a6c840988
commit eafcde8143
9 changed files with 431 additions and 117 deletions
+106 -48
View File
@@ -2,7 +2,7 @@
* CyberSim OS - Immersive Workstation Login & Network Selection Screen (Phase 2.5)
*
* Immersive fictional workstation lockscreen, runtime language switching,
* learner first-name validation, and dynamic workplace network selection.
* learner first & last name validation, and dynamic workplace network selection.
*/
import { i18n } from './i18n.js';
@@ -55,9 +55,11 @@ export class LoginScreen {
const enabledLocales = i18n.getEnabledLocales();
const currentLocale = i18n.getLocale();
// Preserve entered first name if switching language
const existingInput = this.container.querySelector('#login-firstname-input');
const existingName = existingInput ? existingInput.value : '';
// 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 = `
<div class="cs-login-backdrop">
@@ -80,16 +82,24 @@ export class LoginScreen {
</div>
</div>
<!-- Dynamic Language Selector -->
<div class="cs-login-lang-select">
<label for="login-lang-dropdown" style="font-size:11px; color:#94a3b8; margin-right:6px;">${i18n.t('login.language')}:</label>
<select id="login-lang-dropdown" class="cs-select cs-select-sm">
${enabledLocales.map(loc => `
<option value="${loc}" ${loc === currentLocale ? 'selected' : ''}>
${i18n.getLocaleDisplayName(loc)}
</option>
`).join('')}
</select>
<!-- Controls (Language & Verify Shortcut) -->
<div style="display:flex; align-items:center; gap:10px;">
<a href="verify.html" target="_blank" id="btn-login-verify-shortcut" class="cs-btn cs-btn-sm" style="display:inline-flex; align-items:center; gap:6px; background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.12); color:#cbd5e1; text-decoration:none; font-size:11px; padding:5px 10px; border-radius:var(--radius-sm);">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#8b5cf6" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>
<span>${i18n.t('login.verifyCertificate')}</span>
</a>
<!-- Dynamic Language Selector -->
<div class="cs-login-lang-select">
<label for="login-lang-dropdown" style="font-size:11px; color:#94a3b8; margin-right:6px;">${i18n.t('login.language')}:</label>
<select id="login-lang-dropdown" class="cs-select cs-select-sm">
${enabledLocales.map(loc => `
<option value="${loc}" ${loc === currentLocale ? 'selected' : ''}>
${i18n.getLocaleDisplayName(loc)}
</option>
`).join('')}
</select>
</div>
</div>
</div>
@@ -100,24 +110,42 @@ export class LoginScreen {
<p>${i18n.t('login.instructions')}</p>
</div>
<!-- Learner First Name Input -->
<div class="cs-form-group" style="margin-bottom:18px;">
<label class="cs-form-label" for="login-firstname-input">
${i18n.t('login.firstName')} <span style="color:#ef4444;">*</span>
</label>
<input
type="text"
id="login-firstname-input"
class="cs-input"
placeholder="${i18n.t('login.firstNamePlaceholder')}"
maxlength="50"
value="${LoginScreen.escapeHtml(existingName)}"
autocomplete="off"
spellcheck="false"
required
/>
<div id="login-error-msg" class="cs-login-error" style="display:none;"></div>
<!-- Learner Name Inputs -->
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom:18px;">
<div class="cs-form-group">
<label class="cs-form-label" for="login-firstname-input">
${i18n.t('login.firstName')} <span style="color:#ef4444;">*</span>
</label>
<input
type="text"
id="login-firstname-input"
class="cs-input"
placeholder="${i18n.t('login.firstNamePlaceholder')}"
maxlength="50"
value="${LoginScreen.escapeHtml(existingFirstName)}"
autocomplete="off"
spellcheck="false"
required
/>
</div>
<div class="cs-form-group">
<label class="cs-form-label" for="login-lastname-input">
${i18n.t('login.lastName')} <span style="color:#ef4444;">*</span>
</label>
<input
type="text"
id="login-lastname-input"
class="cs-input"
placeholder="${i18n.t('login.lastNamePlaceholder')}"
maxlength="50"
value="${LoginScreen.escapeHtml(existingLastName)}"
autocomplete="off"
spellcheck="false"
required
/>
</div>
</div>
<div id="login-error-msg" class="cs-login-error" style="display:none; margin-bottom:12px;"></div>
<!-- Dynamic Network / Workplace Selection -->
<div class="cs-form-group">
@@ -168,8 +196,12 @@ export class LoginScreen {
</svg>
<span>${i18n.t('login.disclaimer')}</span>
</div>
<div style="margin-top:4px; font-size:10px; color:#64748b;">
${i18n.t('login.sessionInfo')}
<div style="display:flex; justify-content:space-between; align-items:center; margin-top:6px; font-size:10px; color:#64748b;">
<div>${i18n.t('login.sessionInfo')}</div>
<a href="verify.html" target="_blank" style="color:#93c5fd; text-decoration:none; display:inline-flex; align-items:center; gap:4px; font-size:11px;">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>
${i18n.t('login.verifyCertificate')}
</a>
</div>
</div>
</div>
@@ -216,15 +248,24 @@ export class LoginScreen {
});
});
// First name input on Enter
const nameInput = this.container.querySelector('#login-firstname-input');
if (nameInput) {
nameInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
this._handleConnect();
}
});
nameInput.focus();
// 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
@@ -252,24 +293,39 @@ export class LoginScreen {
_handleConnect() {
this._clearError();
const nameInput = this.container.querySelector('#login-firstname-input');
const rawName = nameInput ? nameInput.value : '';
const firstName = rawName.trim();
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 (nameInput) nameInput.focus();
if (firstNameInput) firstNameInput.focus();
return;
}
if (firstName.length > 50) {
this._showError(i18n.t('login.firstNameTooLong'));
if (nameInput) nameInput.focus();
if (firstNameInput) firstNameInput.focus();
return;
}
// 2. Validate Selected Network
// 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'));
@@ -292,6 +348,8 @@ export class LoginScreen {
if (this.onConnect) {
this.onConnect({
firstName,
lastName,
name: `${firstName} ${lastName}`,
scenarioId: selectedScenario.id,
scenarioPath: selectedScenario.path,
locale: currentLocale