Implement Login page with PocketBase auth (P1.6)

Add functional login page with email/password form:
- Client component with controlled form inputs
- PocketBase authentication integration
- Error handling with visual feedback
- Loading states (disabled inputs, button text change)
- Form validation (prevents empty submissions)
- Redirect to dashboard on successful login

Test infrastructure improvements:
- Add @testing-library/jest-dom for DOM matchers
- Add global test setup with cleanup between tests
- Configure vitest.config.ts with setupFiles

14 new tests covering form rendering, auth flow, error
handling, and validation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-10 19:19:50 +00:00
parent 18c34916ca
commit 933e39aed4
7 changed files with 451 additions and 9 deletions

View File

@@ -1,12 +1,111 @@
// ABOUTME: Login page for user authentication.
// ABOUTME: Provides email/password login form using PocketBase auth.
"use client";
import { useRouter } from "next/navigation";
import { type FormEvent, useState } from "react";
import { pb } from "@/lib/pocketbase";
export default function LoginPage() {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Basic validation - don't submit with empty fields
if (!email.trim() || !password.trim()) {
return;
}
setIsLoading(true);
setError(null);
try {
await pb.collection("users").authWithPassword(email, password);
router.push("/");
} catch (err) {
const message =
err instanceof Error ? err.message : "Invalid credentials";
setError(message);
setIsLoading(false);
}
};
const handleInputChange = (
setter: React.Dispatch<React.SetStateAction<string>>,
value: string,
) => {
setter(value);
// Clear error when user starts typing again
if (error) {
setError(null);
}
};
return (
<div className="flex min-h-screen items-center justify-center">
<div className="w-full max-w-md space-y-8 p-8">
<h1 className="text-2xl font-bold text-center">PhaseFlow Login</h1>
{/* Login form will be implemented here */}
<p className="text-center text-gray-500">Login form placeholder</p>
<h1 className="text-2xl font-bold text-center">PhaseFlow</h1>
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div
role="alert"
className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded"
>
{error}
</div>
)}
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700"
>
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => handleInputChange(setEmail, e.target.value)}
disabled={isLoading}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
required
/>
</div>
<div>
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700"
>
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => handleInputChange(setPassword, e.target.value)}
disabled={isLoading}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
required
/>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:bg-blue-400 disabled:cursor-not-allowed"
>
{isLoading ? "Signing in..." : "Sign in"}
</button>
</form>
</div>
</div>
);