import { useEffect, useRef, useState } from 'preact/hooks'; import { apiFetch, ApiError, jsonBody, toApiError } from '../lib/api'; import { setAuthed } from '../lib/auth'; import { currentSearch, didClientNavigate, getQueryParam, navigate, sanitizeNext } from '../router'; import { Spinner } from '../components/Spinner'; import type { User } from '../types'; type Probe = 'checking' | 'connected' | 'unreachable'; interface FieldError { email?: string; password?: string; username?: string; } const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; export function Login() { const [register, setRegister] = useState(() => getQueryParam(currentSearch.value, 'mode') === 'register'); const [email, setEmail] = useState(''); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [submitting, setSubmitting] = useState(false); const [fieldErrors, setFieldErrors] = useState({}); const [serverError, setServerError] = useState(null); const [probe, setProbe] = useState('checking'); const emailRef = useRef(null); const next = sanitizeNext(getQueryParam(currentSearch.value, 'next')); useEffect(() => { probeServer(); // autofocus Email only on client-side navigation, never cold page load if (didClientNavigate) emailRef.current?.focus(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const probeServer = async () => { setProbe('checking'); try { await apiFetch('/web/auth/me', { refreshOn401: false }); setProbe('connected'); } catch { setProbe('unreachable'); } }; const toggleMode = () => { const newMode = !register; setRegister(newMode); setFieldErrors({}); setServerError(null); window.history.replaceState( {}, '', newMode ? '/login?mode=register' : '/login' ); }; const validate = (): boolean => { const errs: FieldError = {}; if (!EMAIL_RE.test(email.trim())) errs.email = 'Enter a valid email address'; if (!password) errs.password = 'Password is required'; if (register && !username.trim()) errs.username = 'Username is required'; setFieldErrors(errs); return Object.keys(errs).length === 0; }; const submit = async (e: Event) => { e.preventDefault(); setServerError(null); if (submitting) return; if (!validate()) return; setSubmitting(true); const body = register ? { username: username.trim(), password, email: email.trim() } : { email: email.trim(), password }; try { const res = await apiFetch(register ? '/web/auth/register' : '/web/auth/login', { refreshOn401: false, ...jsonBody(body), }); if (res.ok) { const data = (await res.json()) as { user?: User }; if (data?.user) { setAuthed(data.user); navigate(next); return; } throw new ApiError(0, 'Something went wrong'); } const err = await toApiError(res); throw err; } catch (err) { const msg = err instanceof ApiError ? err.message : 'Something went wrong'; setServerError(msg); } finally { setSubmitting(false); } }; const origin = typeof window !== 'undefined' ? window.location.origin : ''; return (
Skip to content

KeepNote

Your notes on Dcntrlized

setEmail((e.target as HTMLInputElement).value)} /> {fieldErrors.email && ( )}
{register && (
setUsername((e.target as HTMLInputElement).value)} /> {fieldErrors.username && ( )}
)}
setPassword((e.target as HTMLInputElement).value)} /> {fieldErrors.password && ( )}
{serverError && (
{serverError}
)}
); }