Files
hamlab/web/src/Login.tsx
T
Mitchell-JohnsonandClaude Opus 5 ed64aca63b An eye on the login form, to see what is being typed
A guest password is read off a message and typed by hand, and from the login
form a mistyped character and a wrong password look identical. Revealed, the
field is a text field, so it also turns off capitalisation and autocorrect,
which a phone keyboard would otherwise offer for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 18:03:26 -07:00

62 lines
3.1 KiB
TypeScript

import { FormEvent, useState } from 'react'
import { api } from './api'
import { Spinner } from './ui'
// Drawn, not typed: 👁 is an emoji on phones, full colour and its own size. Outline strokes
// match the glyphs in the rail and the power button.
const EyeIcon = (
<svg viewBox="0 0 16 16" width="17" height="17" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M1.2 8S3.9 3.6 8 3.6 14.8 8 14.8 8 12.1 12.4 8 12.4 1.2 8 1.2 8z" /><circle cx="8" cy="8" r="2.1" />
</svg>
)
const EyeOffIcon = (
<svg viewBox="0 0 16 16" width="17" height="17" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M1.2 8S3.9 3.6 8 3.6 14.8 8 14.8 8 12.1 12.4 8 12.4 1.2 8 1.2 8z" /><circle cx="8" cy="8" r="2.1" /><path d="M2.4 13.6 13.6 2.4" />
</svg>
)
const USER_KEY = 'hamlab.user' // the last account signed in on this device, offered again
export default function Login({ onLogin }: { onLogin: () => void }) {
const [user, setUser] = useState(localStorage.getItem(USER_KEY) ?? '')
const [pw, setPw] = useState('')
const [err, setErr] = useState('')
const [busy, setBusy] = useState(false)
// Shown on request: a generated password is typed by hand off a message, and a wrong
// character looks exactly like a wrong password from here.
const [show, setShow] = useState(false)
async function submit(e: FormEvent) {
e.preventDefault()
setBusy(true); setErr('')
try {
const r = await api.login(user.trim(), pw)
localStorage.setItem(USER_KEY, r?.user ?? user.trim())
onLogin()
} catch (e) { setErr((e instanceof Error && e.message) || 'login failed'); setBusy(false) }
}
return (
<div className="center">
<form className="login" onSubmit={submit}>
<img className="login-logo" src="/icons/logo.png" alt="Ham Lab" width={176} height={176} />
<input type="text" autoFocus={!user} autoComplete="username" placeholder="user" spellCheck={false} autoCapitalize="off" autoCorrect="off"
value={user} onChange={(e) => setUser(e.target.value)} disabled={busy} />
<div className="pwfield">
{/* revealed, the field is a text field, so the keyboard would otherwise offer to
capitalise and correct what is typed into it */}
<input type={show ? 'text' : 'password'} autoFocus={!!user} autoComplete="current-password" placeholder="password"
spellCheck={false} autoCapitalize="off" autoCorrect="off"
value={pw} onChange={(e) => setPw(e.target.value)} disabled={busy} />
<button type="button" className="pweye" onClick={() => setShow(!show)} disabled={busy}
aria-label={show ? 'hide the password' : 'show the password'} aria-pressed={show} title={show ? 'hide' : 'show'}>
{show ? EyeOffIcon : EyeIcon}
</button>
</div>
<button disabled={busy || !pw}>{busy && <Spinner />}{busy ? 'Signing in…' : 'Sign in'}</button>
{err && <div className="error">{err}</div>}
</form>
</div>
)
}