Within my Next.js 13 project, I've implemented a login form structure as outlined below:
"use client";
import * as React from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { signIn } from "next-auth/react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { useSearchParams } from "next/navigation";
import Link from "next/link";
import { cn } from "@/lib/util";
import { userAuthSchema } from "@/lib/validations/auth";
import { buttonVariants } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Icons } from "@/components/icons";
import { loginUser } from "@/lib/login-user";
export type FormData = z.infer<typeof userAuthSchema>;
export function LoginForm() {
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<FormData>({ resolver: zodResolver(userAuthSchema) });
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [error, setError] = React.useState<string | null>(null);
const [isGoogleLoading, setIsGoogleLoading] = React.useState<boolean>(false);
const searchParams = useSearchParams();
async function onSubmit(submittedData: FormData) {
await loginUser({
submittedData,
setError,
setIsLoading,
reset,
searchParams,
});
}
return (
<div className="grid gap-6">
{error && (
<p className="text-sm text-red-500 animate-in fade-in-0 slide-in-from-left-1">
{error}
</p>
)}
<form onSubmit={handleSubmit(onSubmit)}>
<div className="grid gap-6">
<div className="grid gap-1">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="157b74787055706d74786579703b767a78">[email protected]</a>"
autoCapitalize="none"
autoComplete="email"
autoCorrect="off"
disabled={isLoading || isGoogleLoading}
{...register("email")}
/>
{errors.email && (
<p className="px-1 text-xs text-red-600 animate-in fade-in-0 slide-in-from-left-1">
{errors.email.message}
</p>
)}
</div>
<div className="grid gap-1">
<div className="flex items-center justify-between">
<Label htmlFor="password">Password</Label>
<Link
href="/request-password-reset"
className="text-sm font-medium text-sky-700 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-400 focus-visible:ring-offset-2 active:text-sky-400"
>
Forgot password?
</Link>
</div>
<Input
id="password"
type="password"
autoCapitalize="none"
autoComplete="off"
autoCorrect="off"
disabled={isLoading || isGoogleLoading}
{...register("password")}
/>
{errors.password && (
<p className="px-1 text-xs text-red-600 animate-in fade-in-0 slide-in-from-left-1">
{errors.password.message}
</p>
)}
</div>
<button className={cn(buttonVariants())} disabled={isLoading}>
{isLoading && (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
)}
Login
</button>
</div>
</form>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-neutral-200" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-neutral-100 px-2">Or</span>
</div>
</div>
<button
type="button"
className={cn(buttonVariants({ variant: "outline" }), "w-full")}
onClick={() => {
setIsGoogleLoading(true);
signIn("google", {
callbackUrl: searchParams?.get("from") || "/blog",
});
}}
disabled={isLoading || isGoogleLoading}
>
{isGoogleLoading ? (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
) : (
<Icons.google className="mr-2 h-6 w-6" />
)}{" "}
Login with Google
</button>
</div>
);
}
The usage of searchParams
can be noticed in the loginUser()
method. This is an essential argument passed within the onSubmit
event handler. Below is the code snippet that demonstrates this functionality:
async function onSubmit(submittedData: FormData) {
await loginUser({
submittedData,
setError,
setIsLoading,
reset,
searchParams,
});
}
Let's explore the loginUser()
function:
import type { FormData } from "@/components/auth/register-form";
import { signIn } from "next-auth/react";
const RESPONSE_MESSAGES = {
USER_NOT_FOUND: "User not found",
INVALID_PASSWORD: "Invalid password",
LOGIN_SUCCESSFUL: "Logged in successfully",
LOGIN_FAILURE: "Login failed. Please try again.",
EMAIL_NOT_VERIFIED: "Please verify your email",
};
interface LoginUserProps {
submittedData: FormData;
setError: React.Dispatch<React.SetStateAction<string | null>>;
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
reset: () => void;
searchParams: any;
}
export async function loginUser({
submittedData,
setError,
setIsLoading,
reset,
searchParams,
}: LoginUserProps) {
setIsLoading(true);
setError(null);
try {
const response = await fetch("/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(submittedData),
});
const responseData = await response.json();
if (!response.ok) {
throw new Error(RESPONSE_MESSAGES.LOGIN_FAILURE);
}
// Additional logic based on response messages
} catch (error) {
setError((error as Error).message);
} finally {
setIsLoading(false);
}
}
In relation to the type declaration for searchParams
, it is currently set as any
. If you have insights on determining the exact data type, please contribute. Thanks!