125 lines
3.6 KiB
TypeScript
125 lines
3.6 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { signIn } from "next-auth/react";
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { z } from "zod";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { Eye, EyeOff, LogIn } from "lucide-react";
|
|
|
|
const loginSchema = z.object({
|
|
email: z.string().email("Invalid email address"),
|
|
password: z.string().min(1, "Password is required"),
|
|
});
|
|
|
|
type LoginFormValues = z.infer<typeof loginSchema>;
|
|
|
|
export default function LoginPage() {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard";
|
|
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [authError, setAuthError] = useState<string | null>(null);
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
formState: { errors, isSubmitting },
|
|
} = useForm<LoginFormValues>({
|
|
resolver: zodResolver(loginSchema),
|
|
});
|
|
|
|
const onSubmit = async (data: LoginFormValues) => {
|
|
setAuthError(null);
|
|
const result = await signIn("credentials", {
|
|
email: data.email,
|
|
password: data.password,
|
|
redirect: false,
|
|
});
|
|
|
|
if (result?.error) {
|
|
setAuthError("Invalid email or password");
|
|
return;
|
|
}
|
|
|
|
router.push(callbackUrl);
|
|
router.refresh();
|
|
};
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader className="pb-4">
|
|
<CardTitle className="text-xl align-middle text-center">Sign in</CardTitle>
|
|
|
|
</CardHeader>
|
|
|
|
<CardContent>
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="email">Email</Label>
|
|
<Input
|
|
id="email"
|
|
type="email"
|
|
placeholder="you@studio.com"
|
|
autoComplete="email"
|
|
autoFocus
|
|
{...register("email")}
|
|
/>
|
|
{errors.email && (
|
|
<p className="text-xs text-red-400">{errors.email.message}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="password">Password</Label>
|
|
<div className="relative">
|
|
<Input
|
|
id="password"
|
|
type={showPassword ? "text" : "password"}
|
|
placeholder="••••••••"
|
|
autoComplete="current-password"
|
|
className="pr-10"
|
|
{...register("password")}
|
|
/>
|
|
<button
|
|
type="button"
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
onClick={() => setShowPassword((v) => !v)}
|
|
tabIndex={-1}
|
|
>
|
|
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
</button>
|
|
</div>
|
|
{errors.password && (
|
|
<p className="text-xs text-red-400">{errors.password.message}</p>
|
|
)}
|
|
</div>
|
|
|
|
{authError && (
|
|
<p className="text-sm text-red-400 text-center">{authError}</p>
|
|
)}
|
|
|
|
<Button type="submit" className="w-full gap-2" disabled={isSubmitting}>
|
|
<LogIn className="h-4 w-4" />
|
|
{isSubmitting ? "Signing in..." : "Sign in"}
|
|
</Button>
|
|
</form>
|
|
|
|
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|