Sign In
Installation
Add the following Soul components
The sign-in component uses the button, button-link, animated-link, form-status and input components. Make sure you have added them to your project.
Install the following dependencies
npm install @conform-to/zod @conform-to/react zod
Copy and paste the following code into your project
sections/sign-in/index.tsx
import { AnimatedLink } from '@/vibes/soul/primitives/animated-link';import { ButtonLink } from '@/vibes/soul/primitives/button-link';import { SignInAction, SignInForm } from './sign-in-form';export interface SignInProps { title?: string; signUpTitle?: string; signUpDescription?: string; signUpBenefits?: string[]; action: SignInAction; submitLabel?: string; emailLabel?: string; passwordLabel?: string; forgotPasswordHref: string; forgotPasswordLabel?: string; signUpHref?: string;}/** * This component supports various CSS variables for theming. Here's a comprehensive list, along * with their default values: * * ```css * :root { * --sign-in-font-family: var(--font-family-body); * --sign-in-title-font-family: var(--font-family-heading); * --sign-in-title: hsl(var(--foreground)); * --sign-in-description: hsl(var(--contrast-500)); * } * ``` */export function SignIn({ title = 'Sign In', signUpTitle = 'New Customer?', signUpDescription = 'Create an account with us and be able to:', signUpBenefits = [ 'Check out faster', 'Save multiple shipping addresses', 'Access your order history', 'Track new orders', 'Save items to your Wish List', ], action, submitLabel, emailLabel, passwordLabel, forgotPasswordHref = '/forgot-password', forgotPasswordLabel = 'Forgot your password?', signUpHref = '/sign-up',}: SignInProps) { return ( <div className="@container"> <div className="flex flex-col justify-center gap-y-24 px-3 py-10 @xl:flex-row @xl:px-6 @4xl:py-20 @5xl:px-20"> <div className="w-full @xl:max-w-md @xl:border-r @xl:pr-10 @4xl:pr-20"> <h1 className="mb-10 font-[family-name:var(--sign-in-title-font-family,var(--font-family-heading))] text-4xl font-medium leading-none text-[var(--reset-password-title,hsl(var(--foreground)))] @xl:text-5xl"> {title} </h1> <SignInForm action={action} emailLabel={emailLabel} passwordLabel={passwordLabel} submitLabel={submitLabel} /> <AnimatedLink className="mt-4 block w-fit text-sm font-semibold" href={forgotPasswordHref} > {forgotPasswordLabel} </AnimatedLink> </div> <div className="flex w-full flex-col @xl:max-w-md @xl:pl-10 @4xl:pl-20"> <div className="font-[family-name:var(--sign-in-font-family,var(--font-family-body))]"> <h2 className="mb-10 font-[family-name:var(--sign-in-title-font-family,var(--font-family-heading))] text-4xl font-medium leading-none text-[var(--reset-password-title,hsl(var(--foreground)))] @xl:text-5xl"> {signUpTitle} </h2> <div className="text-[var(--sign-in-description,hsl(var(--contrast-500)))]"> <p>{signUpDescription}</p> <ul className="mb-10 ml-4 mt-4 list-disc"> {signUpBenefits.map((benefit, idx) => ( <li key={idx}>{benefit}</li> ))} </ul> <ButtonLink className="mt-auto w-full" href={signUpHref} variant="secondary"> Create Account </ButtonLink> </div> </div> </div> </div> </div> );}
sections/sign-in/sign-in-form.tsx
'use client';import { getFormProps, getInputProps, SubmissionResult, useForm } from '@conform-to/react';import { getZodConstraint, parseWithZod } from '@conform-to/zod';import { useActionState } from 'react';import { useFormStatus } from 'react-dom';import { FormStatus } from '@/vibes/soul/form/form-status';import { Input } from '@/vibes/soul/form/input';import { Button } from '@/vibes/soul/primitives/button';import { schema } from './schema';type Action<State, Payload> = (state: Awaited<State>, payload: Payload) => State | Promise<State>;export type SignInAction = Action<SubmissionResult | null, FormData>;export interface SignInFormProps { action: SignInAction; emailLabel?: string; passwordLabel?: string; submitLabel?: string;}export function SignInForm({ action, emailLabel = 'Email', passwordLabel = 'Password', submitLabel = 'Sign in',}: SignInFormProps) { const [lastResult, formAction] = useActionState(action, null); const [form, fields] = useForm({ lastResult, defaultValue: { email: '', password: '' }, constraint: getZodConstraint(schema), shouldValidate: 'onBlur', shouldRevalidate: 'onInput', onValidate({ formData }) { return parseWithZod(formData, { schema }); }, }); return ( <form {...getFormProps(form)} action={formAction} className="flex grow flex-col gap-5"> <Input {...getInputProps(fields.email, { type: 'text' })} errors={fields.email.errors} key={fields.email.id} label={emailLabel} /> <Input {...getInputProps(fields.password, { type: 'password' })} className="mb-6" errors={fields.password.errors} key={fields.password.id} label={passwordLabel} /> <SubmitButton>{submitLabel}</SubmitButton> {form.errors?.map((error, index) => ( <FormStatus key={index} type="error"> {error} </FormStatus> ))} </form> );}function SubmitButton({ children }: { children: React.ReactNode }) { const { pending } = useFormStatus(); return ( <Button className="mt-auto w-full" loading={pending} type="submit" variant="secondary"> {children} </Button> );}
sections/sign-in/schema.ts
import { z } from 'zod';export const schema = z.object({ email: z.string().email(), password: z.string(),});
Usage
import { SignIn } from '@/vibes/soul/sections/sign-in';import { schema } from '@/vibes/soul/sections/sign-in/schema';import { SubmissionResult } from '@conform-to/react';import { parseWithZod } from '@conform-to/zod';function Usage() { return <SignIn action={signInAction} forgotPasswordHref="#" />;}async function signInAction(lastResult: SubmissionResult | null, formData: FormData) { 'use server'; const submission = parseWithZod(formData, { schema }); if (submission.status !== 'success') { return submission.reply({ formErrors: ['Boom!'] }); } // Simulate a network request await new Promise((resolve) => setTimeout(resolve, 1000)); // const user = await logIn(submission.value) return submission.reply({ resetForm: true });}
API Reference
Prop | Type | Default |
---|---|---|
title | string | 'Sign In' |
signUpTitle | string | 'New Customer?' |
signUpDescription | string | 'Create an account with us and be able to:' |
signUpBenefits | string[] | [ 'Check out faster', 'Save multiple shipping addresses', 'Access your order history', 'Track new orders', 'Save items to your Wish List'] |
action* | SignInAction | |
submitLabel | string | |
emailLabel | string | |
passwordLabel | string | |
forgotPasswordHref* | string | '/forgot-password' |
forgotPasswordLabel | string | 'Forgot your password?' |
signUpHref | string | '/sign-up' |
SignInAction
type Action<State, Payload> = (state: Awaited<State>, payload: Payload) => State | Promise<State>;export type SignInAction = Action<SubmissionResult | null, FormData>;
This component uses Confom to handle form submissions. Refer to the Conform docs for more details.
Here's an example of an action function that does validation and simulates making a network request:
async function signInAction(lastResult: SubmissionResult | null, formData: FormData) { 'use server'; const submission = parseWithZod(formData, { schema }); if (submission.status !== 'success') { return submission.reply({ formErrors: ['Boom!'] }); } // Simulate a network request await new Promise((resolve) => setTimeout(resolve, 1000)); // const user = await logIn(submission.value) return submission.reply({ resetForm: true });}
CSS Variables
This component supports various CSS variables for theming. Here's a comprehensive list.
:root { --sign-in-font-family: var(--font-family-body); --sign-in-title-font-family: var(--font-family-heading); --sign-in-title: hsl(var(--foreground)); --sign-in-description: hsl(var(--contrast-500));}