
Plain sign-in card: a title, email and password fields with a forgot-password link, a sign-in button and a link to create an account, validated with React Hook Form and Zod. Theme tokens only, light and dark.
blocks/login/minimal-login-01/index.tsx
"use client"
import { useId } from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { Controller, useForm } from "react-hook-form"
import { z } from "zod"
import {
Section,
SectionContent,
} from "@/components/editorial/section"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
// The copy: change text here, not in the markup.
const COPY = {
title: "Sign in to Orbcore",
body: "Use the email you signed up with.",
email: "Email",
password: "Password",
forgot: "Forgot your password?",
submit: "Sign in",
signup: { prompt: "New to Orbcore? ", cta: "Create an account" },
errors: {
email: "Enter a valid email address.",
password: "Enter your password.",
},
} as const
const schema = z.object({
email: z.email(COPY.errors.email),
password: z.string().min(1, COPY.errors.password),
})
type Values = z.infer<typeof schema>
export default function MinimalLogin01() {
const id = useId()
const form = useForm<Values>({
resolver: zodResolver(schema),
defaultValues: { email: "", password: "" },
})
// Your sign-in action goes here; until then the form only validates.
function onSubmit(values: Values) {
void values
}
return (
<Section>
<SectionContent>
<Card className="mx-auto w-full max-w-sm">
<CardHeader>
<CardTitle className="text-xl">
<h1>{COPY.title}</h1>
</CardTitle>
<CardDescription>{COPY.body}</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={form.handleSubmit(onSubmit)} noValidate>
<FieldGroup>
<Controller
name="email"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={`${id}-email`}>
{COPY.email}
</FieldLabel>
<Input
{...field}
id={`${id}-email`}
type="email"
autoComplete="email"
required
aria-invalid={fieldState.invalid}
/>
<FieldError errors={[fieldState.error]} />
</Field>
)}
/>
<Controller
name="password"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<div className="flex items-center">
<FieldLabel htmlFor={`${id}-password`}>
{COPY.password}
</FieldLabel>
<a
href="#"
className="ml-auto text-sm underline-offset-4 hover:underline"
>
{COPY.forgot}
</a>
</div>
<Input
{...field}
id={`${id}-password`}
type="password"
autoComplete="current-password"
required
aria-invalid={fieldState.invalid}
/>
<FieldError errors={[fieldState.error]} />
</Field>
)}
/>
<Field>
<Button type="submit">{COPY.submit}</Button>
</Field>
<FieldDescription className="text-center">
{COPY.signup.prompt}
<a href="#">{COPY.signup.cta}</a>
</FieldDescription>
</FieldGroup>
</form>
</CardContent>
</Card>
</SectionContent>
</Section>
)
}