The login page was still on Plan 1a's dark-only palette, so in light theme it rendered a near-black card on a light page with the heading black-on-black and effectively invisible. This is the first screen a client ever sees. Plan 1b Task 13 required the hardcoded-colour grep to return nothing; it was not run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
91 lines
No EOL
2.8 KiB
TypeScript
91 lines
No EOL
2.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
export default function LoginPage() {
|
|
const router = useRouter();
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [pending, setPending] = useState(false);
|
|
|
|
async function onSubmit(event: React.FormEvent) {
|
|
event.preventDefault();
|
|
setPending(true);
|
|
setError(null);
|
|
|
|
const response = await fetch("/api/auth/login", {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ email, password }),
|
|
});
|
|
|
|
setPending(false);
|
|
|
|
if (!response.ok) {
|
|
const body = await response.json().catch(() => ({}));
|
|
setError(body.error ?? "Sign in failed");
|
|
return;
|
|
}
|
|
|
|
router.push("/");
|
|
router.refresh();
|
|
}
|
|
|
|
return (
|
|
<main className="flex min-h-screen items-center justify-center bg-page p-6">
|
|
<form
|
|
onSubmit={onSubmit}
|
|
className="w-full max-w-sm space-y-4 rounded-[var(--radius-panel)] border border-border-subtle bg-panel p-6 shadow-[var(--shadow-panel)]"
|
|
>
|
|
<div className="space-y-1">
|
|
<h1 className="text-lg font-semibold tracking-tight text-text-primary">
|
|
Sign in
|
|
</h1>
|
|
<p className="text-xs text-text-muted">
|
|
Project tracking for client work
|
|
</p>
|
|
</div>
|
|
|
|
<label className="block space-y-1">
|
|
<span className="text-sm text-text-secondary">Email</span>
|
|
<input
|
|
type="email"
|
|
required
|
|
autoComplete="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
className="w-full rounded-lg border border-border-subtle bg-surface px-3 py-2 text-text-primary outline-none focus:border-brand"
|
|
/>
|
|
</label>
|
|
|
|
<label className="block space-y-1">
|
|
<span className="text-sm text-text-secondary">Password</span>
|
|
<input
|
|
type="password"
|
|
required
|
|
autoComplete="current-password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
className="w-full rounded-lg border border-border-subtle bg-surface px-3 py-2 text-text-primary outline-none focus:border-brand"
|
|
/>
|
|
</label>
|
|
|
|
{error && (
|
|
<p role="alert" className="text-sm text-accent">
|
|
{error}
|
|
</p>
|
|
)}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={pending}
|
|
className="w-full rounded-lg bg-brand px-3 py-2 font-medium text-brand-fg transition-colors hover:bg-brand-hover disabled:opacity-50"
|
|
>
|
|
{pending ? "Signing in…" : "Sign in"}
|
|
</button>
|
|
</form>
|
|
</main>
|
|
);
|
|
} |