feat(web-ui): LoginScreen + SetupScreen (layout B from brainstorm)
This commit is contained in:
parent
7e240d86c8
commit
cfe21e315e
1 changed files with 179 additions and 0 deletions
179
services/web-ui/public/screens-auth.jsx
Normal file
179
services/web-ui/public/screens-auth.jsx
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
// LoginScreen + SetupScreen — layout B from the auth brainstorm spec:
|
||||
// 22px wordmark + "WILD DRAGON BROADCAST" tagline above a --bg-1 card.
|
||||
// Matches DESIGN.md tokens; no decoration, dense, ops register.
|
||||
|
||||
(function () {
|
||||
const API_BASE = '/api/v1';
|
||||
|
||||
async function postJson(path, body) {
|
||||
return fetch(API_BASE + path, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'dragonflight-ui',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
function Brand() {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', marginBottom: 22 }}>
|
||||
<div style={{ fontSize: 22, fontWeight: 600, color: 'var(--text-1)', letterSpacing: '-0.01em' }}>Dragonflight</div>
|
||||
<div style={{ fontSize: 10.5, fontWeight: 600, color: 'var(--text-3)', marginTop: 6, letterSpacing: '0.08em', textTransform: 'uppercase' }}>
|
||||
Wild Dragon Broadcast
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Card({ children }) {
|
||||
return (
|
||||
<div style={{
|
||||
background: 'var(--bg-1)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 12,
|
||||
padding: 22,
|
||||
}}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({ label, type = 'text', value, onChange, autoComplete, autoFocus }) {
|
||||
return (
|
||||
<div style={{ marginBottom: 14 }}>
|
||||
<label style={{ display: 'block', fontSize: 10.5, fontWeight: 600, color: 'var(--text-2)', letterSpacing: '0.06em', textTransform: 'uppercase', marginBottom: 6 }}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
type={type}
|
||||
autoComplete={autoComplete}
|
||||
autoFocus={autoFocus}
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
background: 'var(--bg-3)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 4,
|
||||
padding: '8px 10px',
|
||||
fontSize: 12.5,
|
||||
color: 'var(--text-1)',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Button({ children, disabled, onClick, type = 'button' }) {
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
style={{
|
||||
width: '100%',
|
||||
background: disabled ? 'var(--bg-3)' : 'var(--accent)',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: 4,
|
||||
padding: '9px',
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
fontFamily: 'inherit',
|
||||
cursor: disabled ? 'default' : 'pointer',
|
||||
opacity: disabled ? 0.6 : 1,
|
||||
}}
|
||||
>{children}</button>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorRow({ text }) {
|
||||
if (!text) return null;
|
||||
return (
|
||||
<div style={{
|
||||
fontSize: 11.5,
|
||||
color: 'var(--danger)',
|
||||
marginBottom: 12,
|
||||
padding: '6px 10px',
|
||||
background: 'var(--danger-soft)',
|
||||
border: '1px solid var(--danger)',
|
||||
borderRadius: 4,
|
||||
}}>{text}</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Screen({ children }) {
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: 'var(--bg-0)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<form onSubmit={e => e.preventDefault()} style={{ width: 300 }}>
|
||||
<Brand />
|
||||
<Card>{children}</Card>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LoginScreen({ onDone }) {
|
||||
const [username, setUsername] = React.useState('');
|
||||
const [password, setPassword] = React.useState('');
|
||||
const [error, setError] = React.useState('');
|
||||
const [busy, setBusy] = React.useState(false);
|
||||
const submit = async () => {
|
||||
setError(''); setBusy(true);
|
||||
try {
|
||||
const r = await postJson('/auth/login', { username, password });
|
||||
if (r.status === 200) { onDone(); return; }
|
||||
const body = await r.json().catch(() => ({}));
|
||||
setError(body.error || ('Login failed: ' + r.status));
|
||||
} catch (e) { setError(e.message || 'Login failed'); }
|
||||
finally { setBusy(false); }
|
||||
};
|
||||
return (
|
||||
<Screen>
|
||||
<ErrorRow text={error} />
|
||||
<Field label="Username" value={username} onChange={setUsername} autoComplete="username" autoFocus />
|
||||
<Field label="Password" type="password" value={password} onChange={setPassword} autoComplete="current-password" />
|
||||
<Button type="submit" disabled={busy || !username || !password} onClick={submit}>Sign in</Button>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
function SetupScreen({ onDone }) {
|
||||
const [username, setUsername] = React.useState('');
|
||||
const [password, setPassword] = React.useState('');
|
||||
const [confirm, setConfirm] = React.useState('');
|
||||
const [error, setError] = React.useState('');
|
||||
const [busy, setBusy] = React.useState(false);
|
||||
const submit = async () => {
|
||||
setError('');
|
||||
if (password !== confirm) { setError('Passwords do not match'); return; }
|
||||
if (password.length < 12) { setError('Password must be at least 12 characters'); return; }
|
||||
setBusy(true);
|
||||
try {
|
||||
const r = await postJson('/auth/setup', { username, password });
|
||||
if (r.status === 200) { onDone(); return; }
|
||||
const body = await r.json().catch(() => ({}));
|
||||
setError(body.error || ('Setup failed: ' + r.status));
|
||||
} catch (e) { setError(e.message || 'Setup failed'); }
|
||||
finally { setBusy(false); }
|
||||
};
|
||||
return (
|
||||
<Screen>
|
||||
<div style={{ fontSize: 10.5, fontWeight: 600, color: 'var(--text-3)', letterSpacing: '0.08em', textTransform: 'uppercase', marginBottom: 14 }}>
|
||||
First-run setup — create the first admin
|
||||
</div>
|
||||
<ErrorRow text={error} />
|
||||
<Field label="Username" value={username} onChange={setUsername} autoComplete="username" autoFocus />
|
||||
<Field label="Password" type="password" value={password} onChange={setPassword} autoComplete="new-password" />
|
||||
<Field label="Confirm password" type="password" value={confirm} onChange={setConfirm} autoComplete="new-password" />
|
||||
<Button type="submit" disabled={busy || !username || !password || !confirm} onClick={submit}>Create admin</Button>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
window.LoginScreen = LoginScreen;
|
||||
window.SetupScreen = SetupScreen;
|
||||
})();
|
||||
Loading…
Reference in a new issue