diff --git a/services/web-ui/public/screens-auth.jsx b/services/web-ui/public/screens-auth.jsx new file mode 100644 index 0000000..349330d --- /dev/null +++ b/services/web-ui/public/screens-auth.jsx @@ -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 ( +
+
Dragonflight
+
+ Wild Dragon Broadcast +
+
+ ); + } + + function Card({ children }) { + return ( +
{children}
+ ); + } + + function Field({ label, type = 'text', value, onChange, autoComplete, autoFocus }) { + return ( +
+ + 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', + }} + /> +
+ ); + } + + function Button({ children, disabled, onClick, type = 'button' }) { + return ( + + ); + } + + function ErrorRow({ text }) { + if (!text) return null; + return ( +
{text}
+ ); + } + + function Screen({ children }) { + return ( +
+
e.preventDefault()} style={{ width: 300 }}> + + {children} + +
+ ); + } + + 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 ( + + + + + + + ); + } + + 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 ( + +
+ First-run setup — create the first admin +
+ + + + + +
+ ); + } + + window.LoginScreen = LoginScreen; + window.SetupScreen = SetupScreen; +})();