Login template

Overview

This template demonstrates how to build a login form with Neon Design System components and validation. We utilize the react-hook-form library to handle the form state and validation, and the zod library to define the validation schema.

The login process involves two steps:

  1. Verify the email address.
  2. Verify the password or send a Magic Link.

The validation schema for the form is conditionally changed based on these steps.

💡

Note: In this example, the only verified email address is niaz@inspectra.com, which is hard-coded to represent the behavior. In a real application, you would need to verify from the server.

Steps to Build

Import the components

Import the components that from Neon Design System that you will use in your login template along with the react-hook-form and zod libraries for form validation.

import Link from "next/link"
import { zodResolver } from "@hookform/resolvers/zod"
import { MailIcon } from "lucide-react"
import { useForm } from "react-hook-form"
import * as z from "zod"

Define the Login Form with Validation Schema

// ...imports
 
const LoginForm = () => {
  const [verfiedEmail, setVerifiedEmail] = React.useState(false)
  const [loading, setLoading] = React.useState(false)
 
  const static_email = "niaz@inspectra.com"
 
  let FormSchema: z.Schema
 
  if (!verfiedEmail) {
 
    FormSchema = z.object({
      email: z
        .string({
          required_error: "Please fill out this field.",
          invalid_type_error: "Please enter a valid email.",
        })
        .email({
          message: "Please enter a valid email.",
        }),
    })
 
  } else {
 
    FormSchema = z.object({
      email: z
        .string({
          required_error: "Please fill out this field.",
          invalid_type_error: "Please enter a valid email.",
        })
        .email({
          message: "Please enter a valid email.",
        }),
      password: z
        .string({
          required_error: "Please fill out this field.",
          invalid_type_error: "Please enter a valid password.",
        })
        .nonempty({
          message: "Please enter a valid password.",
        }),
    })
 
  }
 
  const form = useForm<z.infer<typeof FormSchema>>({
    resolver: zodResolver(FormSchema),
  })
 
  function onSubmit(data: z.infer<typeof FormSchema>) {
 
    setLoading(true)
 
    setTimeout(() => {
      if (data.email === static_email) {
        setLoading(false)
        setVerifiedEmail(true)
      } else {
        setLoading(false)
        toast.error("Email not found in our database.")
      }
    }, 1000)
 
    if (verfiedEmail) {
      setTimeout(() => {
        setLoading(false)
        toast("You submitted the following values:", {
          description: (
            <pre className="mt-2 p-4">
              <code>{JSON.stringify(data, null, 2)}</code>
            </pre>
          ),
        })
      }, 1000)
    }
 
  }
 
  return (
  //  ...markup
  )
}
 
export default LoginForm

Explanation:

In the example above, we introduce two key states:

  • verifiedEmail: This state serves as a conditional trigger for adjusting the validation schema of the form dynamically as the user progresses through the login process.

  • loading: This state controls the visibility of a loading indicator, which is displayed while the form is being submitted.

Next, we establish an initial validation schema named FormSchema. Initially, it is typed as a generic z.Schema because we intend to modify it based on the verifiedEmail state as the code unfolds.

Email Verification

During the initial phase, the user is prompted to provide their email address for verification. This step aims to confirm whether the provided email address exists in the database or not. If the email address is found in the database, the user will proceed to the next step, which involves entering their password or utilizing the magic link option. However, if the email address is not found in the database, the user will receive an error message, indicating that the provided email is not recognized.

For this first step, the validation schema is focused solely on validating the email address. The schema used for this step is applied when the verifiedEmail state is false.

if (!verfiedEmail) {
  FormSchema = z.object({
    email: z
      .string({
        required_error: "Please fill out this field.",
        invalid_type_error: "Please enter a valid email.",
      })
      .email({
        message: "Please enter a valid email.",
      }),
  })
}

Later this schema will be used in the useForm hook to validate the form. We will be using the zodResolver from @hookform/resolvers/zod library to validate the form. And onSubmit we will check if the email is verified or not. If the email is verified then we will change the verfiedEmail state to true and show the password/magic link or throw an error message.

const form = useForm<z.infer<typeof FormSchema>>({
  resolver: zodResolver(FormSchema),
})
 
function onSubmit(data: z.infer<typeof FormSchema>) {
  setLoading(true)
 
  setTimeout(() => {
    // we are hard coding the email address to represent the behavior
    if (data.email === static_email) {
      setLoading(false)
      setVerifiedEmail(true)
    } else {
      setLoading(false)
      toast.error("Email not found in our database.")
    }
  }, 1000)
 
  if (verfiedEmail) {
    setTimeout(() => {
      setLoading(false)
      toast("You submitted the following values:", {
        description: (
          <pre className="mt-2 p-4">
            <code>{JSON.stringify(data, null, 2)}</code>
          </pre>
        ),
      })
    }, 1000)
  }
}

Verify Password / Send Magic Link

Assuming the user has already confirmed their email address, we'll now proceed with either password verification or sending a magic link. In this second step, we'll utilize the following validation schema when the verifiedEmail state is set to true

FormSchema = z.object({
  email: z
    .string({
      required_error: "Please fill out this field.",
      invalid_type_error: "Please enter a valid email.",
    })
    .email({
      message: "Please enter a valid email.",
    }),
  password: z
    .string({
      required_error: "Please fill out this field.",
      invalid_type_error: "Please enter a valid password.",
    })
    .nonempty({
      message: "Please enter a valid password.",
    }),
})

Once the email is verified, the user can proceed to enter their password and submit the form. A successful password entry will result in the user being logged in, while an incorrect password will trigger an error message.


Alternatively, if the user chooses to use the magic link option, they will receive a notification confirming that a magic link has been sent to their email address. This functionality is seamlessly integrated into the same onSubmit function.

Form Markup

Now that our logic is ready. Here is the markup to work with the form.

<div className="grid h-full md:grid-cols-[0.5fr_1fr]">
  <div className="hidden h-full flex-col justify-between rounded-l-lg bg-gray-12 p-8 text-gray-1 dark:bg-gray-3 dark:text-gray-12 md:flex">
    <h1 className="text-xl font-medium">Acme Inc</h1>
    <p>
      Acme Inc “This library has saved me countless hours of work and helped me
      deliver stunning designs to my clients faster than ever before.”
    </p>
  </div>
  <div className="mx-auto flex h-full flex-col items-center justify-center gap-2 py-32">
    <h1 className="mb-2 text-center text-2xl font-semibold">
      Hey, Welcome Back
    </h1>
    <p className="text-center text-sm text-gray-11">
      Please provide your log in info to access your account.
    </p>
 
    <Form {...form}>
      <form
        onSubmit={form.handleSubmit(onSubmit)}
        className="mt-6 w-full space-y-4 sm:w-[430px]"
      >
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl>
                <Input
                  placeholder="smartroofai@gmail.com"
                  prefix={<MailIcon size={16} />}
                  style={{
                    width: "100%",
                  }}
                  {...field}
                />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
 
        {verfiedEmail && (
          <FormField
            control={form.control}
            name="password"
            render={({ field }) => (
              <FormItem>
                <div className="flex items-center justify-between">
                  <FormLabel>Password</FormLabel>
                  <Link
                    href="/templates/reset-password"
                    className="cursor-pointer text-xs underline underline-offset-2"
                  >
                    Forgot password?
                  </Link>
                </div>
                <FormControl>
                  <Input
                    type="password"
                    placeholder="********"
                    style={{
                      width: "100%",
                    }}
                    {...field}
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
        )}
        <Button loading={loading} className="w-full" type="submit">
          {verfiedEmail ? "Login" : "Continue"}
        </Button>
      </form>
    </Form>
 
    {verfiedEmail && (
      <Link
        href="/templates/magic-link"
        className="mt-6 cursor-pointer text-center text-sm underline underline-offset-2"
      >
        Send me a Magic Link ✨
      </Link>
    )}
 
    <div className="mt-6 flex items-center justify-center gap-2 text-xs">
      Don't have an account?{" "}
      <Link href={"/templates/signup"} className="underline">
        Sign up
      </Link>
    </div>
  </div>
</div>

Done

That's it. You have successfully created a login template with Neon Design System components with validation. You can see the full code at the top of this page.