155 lines
4.1 KiB
TypeScript
155 lines
4.1 KiB
TypeScript
import { z } from "zod";
|
|
import { insertUserSchema, insertContactMessageSchema, insertNewsletterSchema } from "@shared/schema";
|
|
|
|
/**
|
|
* Extended registration schema with password confirmation
|
|
*/
|
|
export const registrationSchema = insertUserSchema.extend({
|
|
password: z.string()
|
|
.min(8, "Password must be at least 8 characters")
|
|
.regex(/[A-Z]/, "Password must contain at least one uppercase letter")
|
|
.regex(/[a-z]/, "Password must contain at least one lowercase letter")
|
|
.regex(/[0-9]/, "Password must contain at least one number"),
|
|
confirmPassword: z.string(),
|
|
}).refine((data) => data.password === data.confirmPassword, {
|
|
message: "Passwords do not match",
|
|
path: ["confirmPassword"],
|
|
});
|
|
|
|
/**
|
|
* Contact form schema with validation
|
|
*/
|
|
export const contactFormSchema = insertContactMessageSchema.extend({
|
|
name: z.string()
|
|
.min(2, "Name must be at least 2 characters")
|
|
.max(100, "Name cannot exceed 100 characters"),
|
|
email: z.string()
|
|
.email("Please enter a valid email address"),
|
|
subject: z.string()
|
|
.min(2, "Subject must be at least 2 characters")
|
|
.max(200, "Subject cannot exceed 200 characters")
|
|
.optional(),
|
|
message: z.string()
|
|
.min(10, "Message must be at least 10 characters")
|
|
.max(2000, "Message cannot exceed 2000 characters"),
|
|
});
|
|
|
|
/**
|
|
* Newsletter signup schema with validation
|
|
*/
|
|
export const newsletterSchema = insertNewsletterSchema.extend({
|
|
email: z.string()
|
|
.email("Please enter a valid email address"),
|
|
agreedToTerms: z.boolean()
|
|
.refine(val => val === true, {
|
|
message: "You must agree to receive emails",
|
|
}),
|
|
});
|
|
|
|
/**
|
|
* Booking form schema
|
|
*/
|
|
export const bookingFormSchema = z.object({
|
|
classId: z.number({
|
|
required_error: "Please select a class",
|
|
}),
|
|
date: z.date({
|
|
required_error: "Please select a date",
|
|
}),
|
|
time: z.string({
|
|
required_error: "Please select a time",
|
|
}),
|
|
});
|
|
|
|
/**
|
|
* Email validation function
|
|
*/
|
|
export function validateEmail(email: string): boolean {
|
|
return z.string().email().safeParse(email).success;
|
|
}
|
|
|
|
/**
|
|
* Password strength validation
|
|
* Returns a score from 0-4 and feedback
|
|
*/
|
|
export function validatePasswordStrength(password: string): {
|
|
score: number;
|
|
feedback: string;
|
|
} {
|
|
// Start with a score of 0
|
|
let score = 0;
|
|
let feedback = "Password is too weak";
|
|
|
|
// If password is empty, return early
|
|
if (!password) {
|
|
return { score, feedback };
|
|
}
|
|
|
|
// Add points for length
|
|
if (password.length >= 8) score += 1;
|
|
if (password.length >= 12) score += 1;
|
|
|
|
// Add points for complexity
|
|
if (/[A-Z]/.test(password)) score += 0.5;
|
|
if (/[a-z]/.test(password)) score += 0.5;
|
|
if (/[0-9]/.test(password)) score += 0.5;
|
|
if (/[^A-Za-z0-9]/.test(password)) score += 0.5;
|
|
|
|
// Provide feedback based on score
|
|
if (score < 1) {
|
|
feedback = "Password is too weak";
|
|
} else if (score < 2) {
|
|
feedback = "Password is weak";
|
|
} else if (score < 3) {
|
|
feedback = "Password is moderate";
|
|
} else if (score < 4) {
|
|
feedback = "Password is strong";
|
|
} else {
|
|
feedback = "Password is very strong";
|
|
}
|
|
|
|
// Ensure score is between 0-4
|
|
score = Math.min(4, Math.max(0, Math.floor(score)));
|
|
|
|
return { score, feedback };
|
|
}
|
|
|
|
/**
|
|
* Format phone number for display
|
|
*/
|
|
export function formatPhoneNumber(phoneNumber: string): string {
|
|
// Remove all non-digits
|
|
const cleaned = phoneNumber.replace(/\D/g, '');
|
|
|
|
// Format based on length
|
|
if (cleaned.length === 10) {
|
|
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6, 10)}`;
|
|
} else if (cleaned.length === 11) {
|
|
return `+${cleaned.slice(0, 1)} (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7, 11)}`;
|
|
} else if (cleaned.length === 12) {
|
|
return `+${cleaned.slice(0, 2)} (${cleaned.slice(2, 5)}) ${cleaned.slice(5, 8)}-${cleaned.slice(8, 12)}`;
|
|
}
|
|
|
|
// Return original if we can't format it
|
|
return phoneNumber;
|
|
}
|
|
|
|
/**
|
|
* Format price for display (e.g., $25.00)
|
|
*/
|
|
export function formatPrice(priceInCents: number): string {
|
|
return `$${(priceInCents / 100).toFixed(2)}`;
|
|
}
|
|
|
|
/**
|
|
* Validate URL function
|
|
*/
|
|
export function validateUrl(url: string): boolean {
|
|
try {
|
|
new URL(url);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|