generated from Labyricorn/labyricorn-project-template
Fix translation fallbacks, start menu lifecycle, add user last name, and verifier certificate print
This commit is contained in:
+106
-48
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user