- Overview: limits hero with progress states, spend pill, primary quick-add, mobile FAB - Logbook: day-grouped entries with subtotals, unified filter bar, Log again, human source labels - Trends: unified chart cards, tick thinning, chart aria summaries, radiogroup forecast control - Settings: sectioned layout with separated danger zone, ThemePicker aria fix - EntryModal: fieldset grouping, Field primitive, safe date validation - AuthView: autoComplete, password visibility toggle - StatHint popover replacing title-only tooltips; mobile stacked import preview - Remaining inline hex styles converted to theme tokens
133 lines
4.9 KiB
TypeScript
133 lines
4.9 KiB
TypeScript
import { Eye, EyeOff, Loader2, LogIn } from "lucide-react";
|
|
import { useState, type CSSProperties, type FormEvent } from "react";
|
|
import { LegalFootnote } from "../LegalFootnote";
|
|
import { ShellBackdrop } from "../ShellBackdrop";
|
|
import type { SetupStatus } from "../../types";
|
|
|
|
type AuthMode = "login" | "signup";
|
|
|
|
type AuthViewProps = {
|
|
authError: string;
|
|
busy: boolean;
|
|
setupStatus: SetupStatus;
|
|
shellStyle: CSSProperties;
|
|
themeId: string;
|
|
resolvedMode: "light" | "dark";
|
|
onLogin: (email: string, password: string) => Promise<void>;
|
|
onSignup: (name: string, email: string, password: string) => Promise<void>;
|
|
};
|
|
|
|
export function AuthView({
|
|
authError,
|
|
busy,
|
|
setupStatus,
|
|
shellStyle,
|
|
themeId,
|
|
resolvedMode,
|
|
onLogin,
|
|
onSignup,
|
|
}: AuthViewProps) {
|
|
const [mode, setMode] = useState<AuthMode>("login");
|
|
const [name, setName] = useState("");
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
|
|
function submit(event: FormEvent<HTMLFormElement>) {
|
|
event.preventDefault();
|
|
if (mode === "signup") {
|
|
void onSignup(name, email, password);
|
|
return;
|
|
}
|
|
void onLogin(email, password);
|
|
}
|
|
|
|
return (
|
|
<div className="app-shell min-h-screen" data-theme={themeId} data-mode={resolvedMode} style={shellStyle}>
|
|
<ShellBackdrop />
|
|
<main className="flex min-h-screen items-center justify-center p-6">
|
|
<div className="auth-panel-shell">
|
|
<div className="mb-8 text-center">
|
|
<h1 className="hero-name text-3xl">Red Bull tracker</h1>
|
|
<p className="hero-copy mt-2 text-sm">Track intake, sync across devices.</p>
|
|
</div>
|
|
|
|
<div className="auth-panel-card">
|
|
{setupStatus.state !== "ok" && (
|
|
<div className="limit-alert mb-4 px-3 py-2 text-xs">
|
|
{setupStatus.message}
|
|
</div>
|
|
)}
|
|
|
|
<div className="auth-mode-toggle mb-5">
|
|
<button className={mode === "login" ? "auth-mode-active" : ""} type="button" onClick={() => setMode("login")}>
|
|
Log in
|
|
</button>
|
|
<button className={mode === "signup" ? "auth-mode-active" : ""} type="button" onClick={() => setMode("signup")}>
|
|
Sign up
|
|
</button>
|
|
</div>
|
|
|
|
<form className="grid gap-3" onSubmit={submit}>
|
|
{mode === "signup" && (
|
|
<label className="field-label">
|
|
Name
|
|
<input className="field-control" type="text" autoComplete="name" value={name} onChange={(event) => setName(event.target.value)} placeholder="Ned" />
|
|
</label>
|
|
)}
|
|
<label className="field-label">
|
|
Email
|
|
<input className="field-control" type="email" autoComplete="email" value={email} onChange={(event) => setEmail(event.target.value)} placeholder="you@example.com" required />
|
|
</label>
|
|
<label className="field-label">
|
|
Password
|
|
<span className="relative block">
|
|
<input
|
|
className="field-control pr-12"
|
|
minLength={8}
|
|
type={showPassword ? "text" : "password"}
|
|
autoComplete={mode === "signup" ? "new-password" : "current-password"}
|
|
value={password}
|
|
onChange={(event) => setPassword(event.target.value)}
|
|
placeholder="8+ characters"
|
|
required
|
|
/>
|
|
<button
|
|
className="password-toggle"
|
|
type="button"
|
|
aria-label={showPassword ? "Hide password" : "Show password"}
|
|
aria-pressed={showPassword}
|
|
onClick={() => setShowPassword((current) => !current)}
|
|
>
|
|
{showPassword ? <EyeOff size={17} aria-hidden="true" /> : <Eye size={17} aria-hidden="true" />}
|
|
</button>
|
|
</span>
|
|
</label>
|
|
|
|
{authError && (
|
|
<div
|
|
className="rounded-md px-3 py-2 text-sm"
|
|
style={{
|
|
border: "1px solid color-mix(in srgb, var(--error) 35%, transparent)",
|
|
background: "var(--error-container)",
|
|
color: "var(--on-error-container)",
|
|
}}
|
|
>
|
|
{authError}
|
|
</div>
|
|
)}
|
|
|
|
<button className="primary-button w-full mt-1" type="submit" disabled={busy}>
|
|
{busy ? <Loader2 className="animate-spin" size={17} aria-hidden="true" /> : <LogIn size={17} aria-hidden="true" />}
|
|
{mode === "signup" ? "Create account" : "Log in"}
|
|
</button>
|
|
</form>
|
|
|
|
<LegalFootnote className="mt-5" />
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|