Files
grabowski 65480fd892 Auto-format token and join-code inputs with dashes while typing
Shared wireCodeMask helper: uppercase, dash every 4 chars, raw-length
cap, caret preserved, backspace over a dash deletes the char before it.
2026-07-20 10:46:13 +07:00

165 lines
5.1 KiB
JavaScript

// Login view — Mullvad-style account tokens (no username/password).
// Two panels: create a new account (one-time token reveal) or log in with an
// existing account number.
import { api } from '../api.js';
import { el, mount, toast, wireCodeMask } from '../dom.js';
import { groupCode, normalizeCode } from '../format.js';
export function renderAuth(container, ctx) {
let mode = 'create'; // 'create' | 'login'
function draw() {
const card = el(
'div',
{ class: 'auth-card card' },
el(
'div',
{ class: 'auth-head' },
el('div', { class: 'brand-mark brand-mark-lg' }, '🧭'),
el('h1', {}, 'Trip Plan'),
el('p', { class: 'muted' }, 'Plan trips together, day by day.'),
),
el(
'div',
{ class: 'auth-tabs' },
tab('Create account', mode === 'create', () => setMode('create')),
tab('Log in', mode === 'login', () => setMode('login')),
),
mode === 'create' ? createPanel() : loginPanel(),
);
mount(container, el('div', { class: 'auth-wrap' }, card));
}
function setMode(next) {
if (mode !== next) {
mode = next;
draw();
}
}
function tab(label, active, onClick) {
return el('button', { class: `auth-tab${active ? ' active' : ''}`, type: 'button', onClick }, label);
}
// ----- Create account -----
function createPanel() {
const errorEl = el('p', { class: 'form-error' });
const createBtn = el('button', { class: 'btn btn-primary btn-block', type: 'button' }, 'Create account');
async function onCreate() {
errorEl.textContent = '';
createBtn.disabled = true;
createBtn.textContent = 'Creating…';
try {
const data = await api.auth.createAccount();
showToken(data.token, data.user);
} catch (err) {
errorEl.textContent = err.message;
createBtn.disabled = false;
createBtn.textContent = 'Create account';
}
}
createBtn.addEventListener('click', onCreate);
return el(
'div',
{ class: 'auth-panel' },
el('p', { class: 'auth-lead' }, 'No email, no password. We generate a private account number — it is your only key to your trips.'),
errorEl,
createBtn,
);
}
function showToken(token, user) {
const grouped = groupCode(token, 4);
const copyBtn = el('button', { class: 'btn', type: 'button' }, '📋 Copy');
copyBtn.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(grouped);
toast('Account number copied', 'success');
} catch {
toast('Copy failed — select and copy it manually');
}
});
const panel = el(
'div',
{ class: 'auth-panel token-reveal' },
el('div', { class: 'token-warning' },
el('strong', {}, 'This is your only credential.'),
' Save it now — it will never be shown again. Anyone with it can access your trips.'),
el('div', { class: 'token-box' }, grouped),
el('div', { class: 'token-actions' }, copyBtn),
el(
'button',
{
class: 'btn btn-primary btn-block',
type: 'button',
onClick: () => {
ctx.state.user = user;
ctx.navigate('#/trips');
},
},
"I've saved it — continue",
),
);
mount(container, el('div', { class: 'auth-wrap' }, el('div', { class: 'auth-card card' },
el('div', { class: 'auth-head' },
el('div', { class: 'brand-mark brand-mark-lg' }, '🎉'),
el('h1', {}, 'Account created'),
el('p', { class: 'muted' }, `You're ${user.display_name}. You can rename yourself later.`)),
panel,
)));
}
// ----- Log in -----
function loginPanel() {
const errorEl = el('p', { class: 'form-error' });
const input = el('input', {
class: 'input token-input',
type: 'text',
autocomplete: 'off',
autocapitalize: 'characters',
spellcheck: 'false',
placeholder: 'XXXX-XXXX-XXXX-XXXX',
maxlength: '19',
'aria-label': 'Account number',
});
wireCodeMask(input, 16);
const submitBtn = el('button', { class: 'btn btn-primary btn-block', type: 'submit' }, 'Log in');
async function onSubmit(e) {
e.preventDefault();
errorEl.textContent = '';
const token = normalizeCode(input.value);
if (token.length < 8) {
errorEl.textContent = 'Enter your full account number.';
return;
}
submitBtn.disabled = true;
submitBtn.textContent = 'Signing in…';
try {
const data = await api.auth.login(token);
ctx.state.user = data.user;
ctx.navigate('#/trips');
} catch (err) {
errorEl.textContent = err.message;
submitBtn.disabled = false;
submitBtn.textContent = 'Log in';
}
}
return el(
'form',
{ class: 'auth-panel auth-form', onSubmit },
el('label', { class: 'field' }, el('span', { class: 'field-label' }, 'Account number'), input),
el('p', { class: 'hint muted' }, 'Dashes, spaces and letter case do not matter.'),
errorEl,
submitBtn,
);
}
draw();
}