Mercurial
view mrjunejune/src/login/index.html @ 272:41a49c29a28f
polish JRPG conversation experience
Integrate desktop conversations into the utility panel, simplify the mobile frame, add modal destinations and a reusable Zenbu composer lab, and preserve explicit conversation resume behavior.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Fri, 07 Aug 2026 16:05:29 -0700 |
| parents | 04fee26ecce0 |
| children |
line wrap: on
line source
<!DOCTYPE html> <html lang="en" data-zen-theme="cyberpunk"> <head> {{/parts/base_head.html}} <title>Sign in — MrJuneJune</title> <style> .login-page main { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; padding: var(--zenbu-sys-padding-lg) var(--zenbu-sys-padding-md); } .login-card { width: 100%; max-width: 360px; } .login-card h2 { margin: 0 0 var(--zenbu-sys-padding-md); font-size: 1.4rem; font-weight: 700; text-align: center; letter-spacing: 0.08em; } .login-form { display: flex; flex-direction: column; gap: var(--zenbu-sys-padding-sm); } .login-actions { display: flex; justify-content: flex-end; gap: var(--zenbu-sys-space-control); margin-top: var(--zenbu-sys-padding-xs, 0.5rem); } #loginError { display: none; padding: 0.6rem 0.75rem; border-left: var(--zenbu-sys-stroke-width-emphasis, 2px) solid var(--zenbu-sys-color-danger-foreground); background: color-mix(in srgb, var(--zenbu-sys-color-danger-foreground) 10%, transparent); color: var(--zenbu-sys-color-danger-foreground); font-size: 0.875rem; } #loginError[aria-hidden="false"] { display: block; } </style> </head> <body class="login-page"> {{/parts/header.html}} <main> <div class="login-card"> <zen-heading size="xl"> <h2>Sign in</h2> </zen-heading> <div id="loginError" role="alert" aria-live="assertive" aria-hidden="true"></div> <form class="login-form" id="loginForm" novalidate> <zen-field appearance="plain" size="md"> <label for="username">Username</label> <input id="username" name="username" type="text" autocomplete="username" autocapitalize="none" spellcheck="false" required maxlength="32" > </zen-field> <zen-field appearance="plain" size="md"> <label for="password">Password</label> <input id="password" name="password" type="password" autocomplete="current-password" required minlength="12" maxlength="1024" > </zen-field> <div class="login-actions"> <zen-button appearance="plain" size="md"> <button type="submit" id="loginSubmit">Sign in</button> </zen-button> </div> </form> </div> </main> <script> (function () { 'use strict'; const form = document.getElementById('loginForm'); const errorEl = document.getElementById('loginError'); const submitBtn = document.getElementById('loginSubmit'); function showError(message) { errorEl.textContent = message; errorEl.setAttribute('aria-hidden', 'false'); } function hideError() { errorEl.textContent = ''; errorEl.setAttribute('aria-hidden', 'true'); } async function fetchSession() { const resp = await fetch('/api/auth/session', { credentials: 'same-origin', }); if (!resp.ok) throw new Error('session unavailable'); return resp.json(); } form.addEventListener('submit', async function (evt) { evt.preventDefault(); hideError(); const username = form.username.value.trim(); const password = form.password.value; if (!username || !password) { showError('Please enter your username and password.'); return; } submitBtn.disabled = true; try { const session = await fetchSession(); const csrfToken = session.csrfToken; const resp = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Origin': window.location.origin, }, credentials: 'same-origin', body: JSON.stringify({ username, password, csrfToken }), }); form.password.value = ''; let data = await resp.json().catch(function () { return null; }); if (!resp.ok) { showError(resp.status === 429 ? 'Too many attempts. Please try again later.' : 'Invalid username or password.'); return; } if (!data) { data = await fetchSession(); if (data.kind !== 'user') { window.location.reload(); return; } } if (data.mustChangePassword) { window.location.href = '/account/password'; return; } function safeNext(n) { if (!n || typeof n !== 'string') return null; if (!n.startsWith('/') || n.startsWith('//') || n.startsWith('/\\')) return null; if (n.includes('\\')) return null; try { const url = new URL(n, window.location.origin); if (url.origin !== window.location.origin) return null; return url.pathname + url.search + url.hash; } catch (_) { return null; } } const next = safeNext(new URLSearchParams(window.location.search).get('next')); window.location.href = next || '/jrpg'; } catch (_) { form.password.value = ''; showError('Sign-in failed. Please try again.'); } finally { submitBtn.disabled = false; } }); })(); </script> </body> </html>