87 lines
2.7 KiB
TypeScript
87 lines
2.7 KiB
TypeScript
"use client";
|
|
|
|
import { Suspense, useState } from "react";
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
import { AuthCard, AuthFooterLink } from "@/components/auth/auth-card";
|
|
import { useAuth } from "@/components/auth/auth-provider";
|
|
import { AuthError } from "@/lib/auth";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
function LoginForm() {
|
|
const router = useRouter();
|
|
const params = useSearchParams();
|
|
const { logIn } = useAuth();
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
async function onSubmit(e: React.SyntheticEvent) {
|
|
e.preventDefault();
|
|
setError(null);
|
|
setSubmitting(true);
|
|
try {
|
|
await logIn(email, password);
|
|
router.push(params.get("next") ?? "/profile");
|
|
router.refresh();
|
|
} catch (err) {
|
|
setError(err instanceof AuthError ? err.message : "Login failed.");
|
|
setSubmitting(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<AuthCard
|
|
title="Welcome back"
|
|
description="Log in to access your bookmarks, notes, and collections."
|
|
footer={
|
|
<div className="space-y-1">
|
|
<AuthFooterLink prefix="No account?" href="/signup" label="Create one" />
|
|
<AuthFooterLink prefix="Forgot your password?" href="/forgot-password" label="Reset it" />
|
|
</div>
|
|
}
|
|
>
|
|
<form onSubmit={onSubmit} className="space-y-6">
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="email">Email</Label>
|
|
<Input
|
|
id="email"
|
|
type="email"
|
|
autoComplete="email"
|
|
required
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="password">Password</Label>
|
|
<Input
|
|
id="password"
|
|
type="password"
|
|
autoComplete="current-password"
|
|
required
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
/>
|
|
</div>
|
|
<br />
|
|
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
|
<Button type="submit" disabled={submitting} className="w-full h-11 text-base">
|
|
{submitting ? "Logging in…" : "Log in"}
|
|
</Button>
|
|
</form>
|
|
</AuthCard>
|
|
);
|
|
}
|
|
|
|
export default function LoginPage() {
|
|
// useSearchParams must be inside Suspense for Next.js streaming.
|
|
return (
|
|
<Suspense fallback={null}>
|
|
<LoginForm />
|
|
</Suspense>
|
|
);
|
|
}
|