mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-14 17:54:42 +00:00
Declare the register steps and the schema slice each one collects
This commit is contained in:
49
web/frontend/app/features/auth/registerSteps.ts
Normal file
49
web/frontend/app/features/auth/registerSteps.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { z } from 'zod';
|
||||||
|
import { registerFields, type RegisterSchema } from './schemas';
|
||||||
|
|
||||||
|
interface RegisterStep {
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
/** The slice of the register schema this step collects. */
|
||||||
|
schema: z.ZodObject;
|
||||||
|
fields: readonly (keyof RegisterSchema)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const defineStep = (step: Omit<RegisterStep, 'fields'>): RegisterStep => ({
|
||||||
|
...step,
|
||||||
|
fields: Object.keys(step.schema.shape) as (keyof RegisterSchema)[],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const registerSteps: readonly RegisterStep[] = [
|
||||||
|
defineStep({
|
||||||
|
name: 'Account',
|
||||||
|
title: 'Create your account',
|
||||||
|
subtitle: 'Choose how you will sign in.',
|
||||||
|
schema: registerFields.pick({ username: true, email: true }),
|
||||||
|
}),
|
||||||
|
defineStep({
|
||||||
|
name: 'Personal details',
|
||||||
|
title: 'About you',
|
||||||
|
subtitle: 'Tell us a little about yourself.',
|
||||||
|
schema: registerFields.pick({ firstName: true, lastName: true, dateOfBirth: true }),
|
||||||
|
}),
|
||||||
|
defineStep({
|
||||||
|
name: 'Password',
|
||||||
|
title: 'Secure your account',
|
||||||
|
subtitle: 'Pick a strong password.',
|
||||||
|
schema: registerFields.pick({ password: true, confirmPassword: true }),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const lastRegisterStepIndex = registerSteps.length - 1;
|
||||||
|
|
||||||
|
/** The step a field belongs to, so an error can open the step that owns it. */
|
||||||
|
export const stepIndexForField = (field: keyof RegisterSchema) =>
|
||||||
|
registerSteps.findIndex((step) => step.fields.includes(field));
|
||||||
|
|
||||||
|
/** The earliest step owning any of `fields`, or `null` when none of them do. */
|
||||||
|
export const earliestStepForFields = (fields: readonly (keyof RegisterSchema)[]) => {
|
||||||
|
const indexes = fields.map(stepIndexForField).filter((index) => index >= 0);
|
||||||
|
return indexes.length ? Math.min(...indexes) : null;
|
||||||
|
};
|
||||||
@@ -7,27 +7,53 @@ export const loginSchema = z.object({
|
|||||||
|
|
||||||
export type LoginSchema = z.infer<typeof loginSchema>;
|
export type LoginSchema = z.infer<typeof loginSchema>;
|
||||||
|
|
||||||
export const registerSchema = z
|
export const minimumAgeYears = 19;
|
||||||
.object({
|
|
||||||
username: z
|
const isOldEnough = (value: string) => {
|
||||||
.string()
|
const dateOfBirth = new Date(value);
|
||||||
.min(3, 'Username must be at least 3 characters')
|
if (Number.isNaN(dateOfBirth.getTime())) return false;
|
||||||
.max(20, 'Username must be at most 20 characters'),
|
|
||||||
firstName: z.string().min(1, 'First name is required'),
|
const comingOfAge = new Date(
|
||||||
lastName: z.string().min(1, 'Last name is required'),
|
dateOfBirth.getFullYear() + minimumAgeYears,
|
||||||
email: z.string().email('Invalid email address'),
|
dateOfBirth.getMonth(),
|
||||||
dateOfBirth: z.string().min(1, 'Date of birth is required'),
|
dateOfBirth.getDate(),
|
||||||
password: z
|
);
|
||||||
.string()
|
|
||||||
.min(8, 'Password must be at least 8 characters')
|
return comingOfAge <= new Date();
|
||||||
.regex(/[A-Z]/, 'Password must contain an uppercase letter')
|
};
|
||||||
.regex(/[a-z]/, 'Password must contain a lowercase letter')
|
|
||||||
.regex(/[0-9]/, 'Password must contain a number'),
|
export const registerFields = z.object({
|
||||||
confirmPassword: z.string().min(1, 'Please confirm your password'),
|
username: z
|
||||||
})
|
.string()
|
||||||
.refine((data) => data.password === data.confirmPassword, {
|
.min(3, 'Username must be at least 3 characters')
|
||||||
|
.max(64, 'Username must be at most 64 characters')
|
||||||
|
.regex(
|
||||||
|
/^[A-Za-z0-9._-]+$/,
|
||||||
|
'Username can only contain letters, numbers, dots, underscores and hyphens',
|
||||||
|
),
|
||||||
|
firstName: z.string().min(1, 'First name is required'),
|
||||||
|
lastName: z.string().min(1, 'Last name is required'),
|
||||||
|
email: z.email('Invalid email address'),
|
||||||
|
dateOfBirth: z
|
||||||
|
.string()
|
||||||
|
.min(1, 'Date of birth is required')
|
||||||
|
.refine(isOldEnough, `You must be at least ${minimumAgeYears} years old to register`),
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(8, 'Password must be at least 8 characters')
|
||||||
|
.regex(/[A-Z]/, 'Password must contain an uppercase letter')
|
||||||
|
.regex(/[a-z]/, 'Password must contain a lowercase letter')
|
||||||
|
.regex(/[0-9]/, 'Password must contain a number')
|
||||||
|
.regex(/[^A-Za-z0-9]/, 'Password must contain a special character'),
|
||||||
|
confirmPassword: z.string().min(1, 'Please confirm your password'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const registerSchema = registerFields.refine(
|
||||||
|
(data) => data.password === data.confirmPassword,
|
||||||
|
{
|
||||||
message: 'Passwords must match',
|
message: 'Passwords must match',
|
||||||
path: ['confirmPassword'],
|
path: ['confirmPassword'],
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export type RegisterSchema = z.infer<typeof registerSchema>;
|
export type RegisterSchema = z.infer<typeof registerSchema>;
|
||||||
|
|||||||
Reference in New Issue
Block a user