Spaces:
Sleeping
Sleeping
| <html lang="et"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>ATOM · Login</title> | |
| <style> | |
| :root { --green:#059669; --bg:#f8fafc; --card:#fff; --text:#0f172a; --muted:#64748b; --err:#dc3545; } | |
| * { box-sizing:border-box; } | |
| body { margin:0; min-height:100vh; font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif; | |
| background:linear-gradient(145deg,#ecfdf5 0%,#f0f9ff 50%,#f8fafc 100%); color:var(--text); | |
| display:flex; align-items:center; justify-content:center; padding:1rem; } | |
| .card { width:100%; max-width:420px; background:var(--card); border-radius:16px; padding:2rem; | |
| box-shadow:0 20px 50px rgba(15,23,42,.08); border:1px solid #e2e8f0; } | |
| h1 { margin:0 0 .25rem; font-size:1.75rem; color:var(--green); } | |
| p.sub { margin:0 0 1.5rem; color:var(--muted); } | |
| label { display:block; font-size:.875rem; font-weight:600; margin:.75rem 0 .35rem; } | |
| input { width:100%; padding:.7rem .85rem; border:1px solid #cbd5e1; border-radius:10px; font-size:1rem; } | |
| input:focus { outline:2px solid #6ee7b7; border-color:var(--green); } | |
| button { width:100%; margin-top:1.25rem; padding:.85rem; border:0; border-radius:10px; | |
| background:var(--green); color:#fff; font-weight:700; font-size:1rem; cursor:pointer; min-height:44px; } | |
| button:disabled { opacity:.6; cursor:not-allowed; } | |
| button.secondary { background:transparent; color:var(--green); border:1px solid var(--green); margin-top:.75rem; } | |
| .err { background:#fef2f2; color:var(--err); border:1px solid #fecaca; padding:.75rem; border-radius:10px; | |
| margin-bottom:1rem; font-size:.9rem; display:none; } | |
| .ok { background:#ecfdf5; color:#065f46; border:1px solid #a7f3d0; padding:.75rem; border-radius:10px; | |
| margin-bottom:1rem; font-size:.9rem; display:none; } | |
| .row { display:grid; grid-template-columns:1fr 1fr; gap:.75rem; } | |
| .hint { margin-top:1rem; font-size:.8rem; color:var(--muted); text-align:center; } | |
| a { color:var(--green); } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="card"> | |
| <h1>ATOM</h1> | |
| <p class="sub" id="subtitle">Logi sisse oma kontoga</p> | |
| <div class="err" id="err" role="alert"></div> | |
| <div class="ok" id="ok" role="status"></div> | |
| <form id="form" novalidate> | |
| <div id="names" class="row" style="display:none"> | |
| <div> | |
| <label for="first_name">Eesnimi</label> | |
| <input id="first_name" name="first_name" autocomplete="given-name" /> | |
| </div> | |
| <div> | |
| <label for="last_name">Perekonnanimi</label> | |
| <input id="last_name" name="last_name" autocomplete="family-name" /> | |
| </div> | |
| </div> | |
| <label for="email">Email</label> | |
| <input id="email" name="email" type="email" required autocomplete="username" placeholder="sina@email.ee" /> | |
| <label for="password">Parool</label> | |
| <input id="password" name="password" type="password" required autocomplete="current-password" minlength="6" /> | |
| <button type="submit" id="submit">Logi sisse</button> | |
| <button type="button" class="secondary" id="toggle">Loo uus konto</button> | |
| </form> | |
| <p class="hint">API: <a href="/docs">/docs</a> · Health: <a href="/api/healthz">/api/healthz</a> · <a href="/">Avaleht</a></p> | |
| </div> | |
| <script> | |
| const API = ''; // same origin on HF Space | |
| let mode = 'login'; // or register | |
| const err = document.getElementById('err'); | |
| const ok = document.getElementById('ok'); | |
| const names = document.getElementById('names'); | |
| const subtitle = document.getElementById('subtitle'); | |
| const submit = document.getElementById('submit'); | |
| const toggle = document.getElementById('toggle'); | |
| function showErr(msg) { err.style.display='block'; ok.style.display='none'; err.textContent=msg; } | |
| function showOk(msg) { ok.style.display='block'; err.style.display='none'; ok.textContent=msg; } | |
| function clearMsg() { err.style.display='none'; ok.style.display='none'; } | |
| function formatDetail(data) { | |
| if (!data) return 'Viga'; | |
| const d = data.detail ?? data.message ?? data.error; | |
| if (typeof d === 'string') return d; | |
| if (Array.isArray(d)) return d.map(x => x.msg || JSON.stringify(x)).join('; '); | |
| return JSON.stringify(d); | |
| } | |
| toggle.onclick = () => { | |
| mode = mode === 'login' ? 'register' : 'login'; | |
| clearMsg(); | |
| names.style.display = mode === 'register' ? 'grid' : 'none'; | |
| subtitle.textContent = mode === 'login' ? 'Logi sisse oma kontoga' : 'Loo uus ATOM konto'; | |
| submit.textContent = mode === 'login' ? 'Logi sisse' : 'Registreeri'; | |
| toggle.textContent = mode === 'login' ? 'Loo uus konto' : 'Tagasi sisselogimisele'; | |
| }; | |
| document.getElementById('form').onsubmit = async (e) => { | |
| e.preventDefault(); | |
| clearMsg(); | |
| submit.disabled = true; | |
| const email = document.getElementById('email').value.trim(); | |
| const password = document.getElementById('password').value; | |
| try { | |
| let res, body; | |
| if (mode === 'login') { | |
| res = await fetch(API + '/api/auth/login', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ username: email, password }) | |
| }); | |
| } else { | |
| body = { | |
| email, password, | |
| first_name: document.getElementById('first_name').value.trim() || 'User', | |
| last_name: document.getElementById('last_name').value.trim() || 'Atom' | |
| }; | |
| res = await fetch(API + '/api/auth/register', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(body) | |
| }); | |
| } | |
| const data = await res.json().catch(() => ({})); | |
| if (!res.ok) throw new Error(formatDetail(data) || ('HTTP ' + res.status)); | |
| if (!data.access_token) throw new Error('Token puudub vastuses'); | |
| localStorage.setItem('token', data.access_token); | |
| localStorage.setItem('auth_token', data.access_token); | |
| showOk('Sisselogimine õnnestus — suunan ATOM Command Centerisse…'); | |
| setTimeout(() => { location.href = '/atom.html'; }, 600); | |
| } catch (ex) { | |
| showErr(ex.message || String(ex)); | |
| } finally { | |
| submit.disabled = false; | |
| } | |
| }; | |
| </script> | |
| </body> | |
| </html> | |