united-tattoo/lib/validations.ts

266 lines
11 KiB
TypeScript

import { z } from "zod"
import { UserRole, AppointmentStatus } from "@/types/database"
// User validation schemas
export const userSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1, "Name is required"),
role: z.nativeEnum(UserRole),
avatar: z.string().url().optional(),
})
export const createUserSchema = z.object({
email: z.string().email("Invalid email address"),
name: z.string().min(1, "Name is required").max(100, "Name too long"),
password: z.string().min(8, "Password must be at least 8 characters"),
role: z.nativeEnum(UserRole).default(UserRole.CLIENT),
})
export const updateUserSchema = createUserSchema.partial().extend({
id: z.string().uuid(),
})
// Artist validation schemas
export const artistSchema = z.object({
id: z.string().uuid(),
userId: z.string().uuid(),
name: z.string().min(1, "Artist name is required"),
bio: z.string().min(10, "Bio must be at least 10 characters"),
specialties: z.array(z.string()).min(1, "At least one specialty is required"),
instagramHandle: z.string().optional(),
isActive: z.boolean().default(true),
hourlyRate: z.number().positive().optional(),
})
export const createArtistSchema = z.object({
name: z.string().min(1, "Artist name is required").max(100, "Name too long"),
bio: z.string().min(10, "Bio must be at least 10 characters").max(1000, "Bio too long"),
specialties: z.array(z.string().min(1)).min(1, "At least one specialty is required").max(10, "Too many specialties"),
instagramHandle: z.string().regex(/^[a-zA-Z0-9._]+$/, "Invalid Instagram handle").optional(),
hourlyRate: z.number().positive("Hourly rate must be positive").max(1000, "Hourly rate too high").optional(),
isActive: z.boolean().default(true),
})
export const updateArtistSchema = createArtistSchema.partial().extend({
id: z.string().uuid(),
})
// Portfolio image validation schemas
export const portfolioImageSchema = z.object({
id: z.string().uuid(),
artistId: z.string().uuid(),
url: z.string().url("Invalid image URL"),
caption: z.string().max(500, "Caption too long").optional(),
tags: z.array(z.string()).max(20, "Too many tags"),
order: z.number().int().min(0),
isPublic: z.boolean().default(true),
})
export const createPortfolioImageSchema = z.object({
artistId: z.string().uuid(),
url: z.string().url("Invalid image URL"),
caption: z.string().max(500, "Caption too long").optional(),
tags: z.array(z.string().min(1)).max(20, "Too many tags").default([]),
order: z.number().int().min(0).default(0),
isPublic: z.boolean().default(true),
})
export const updatePortfolioImageSchema = createPortfolioImageSchema.partial().extend({
id: z.string().uuid(),
})
// Appointment validation schemas
export const appointmentSchema = z.object({
id: z.string().uuid(),
artistId: z.string().uuid(),
clientId: z.string().uuid(),
title: z.string().min(1, "Title is required"),
description: z.string().optional(),
startTime: z.date(),
endTime: z.date(),
status: z.nativeEnum(AppointmentStatus),
depositAmount: z.number().positive().optional(),
totalAmount: z.number().positive().optional(),
notes: z.string().optional(),
})
export const createAppointmentSchema = z.object({
artistId: z.string().uuid("Invalid artist ID"),
clientId: z.string().uuid("Invalid client ID"),
title: z.string().min(1, "Title is required").max(200, "Title too long"),
description: z.string().max(1000, "Description too long").optional(),
startTime: z.string().datetime("Invalid start time"),
endTime: z.string().datetime("Invalid end time"),
depositAmount: z.number().positive("Deposit must be positive").optional(),
totalAmount: z.number().positive("Total amount must be positive").optional(),
notes: z.string().max(1000, "Notes too long").optional(),
}).refine(
(data) => new Date(data.endTime) > new Date(data.startTime),
{
message: "End time must be after start time",
path: ["endTime"],
}
)
export const updateAppointmentSchema = z.object({
id: z.string().uuid(),
artistId: z.string().uuid("Invalid artist ID").optional(),
clientId: z.string().uuid("Invalid client ID").optional(),
title: z.string().min(1, "Title is required").max(200, "Title too long").optional(),
description: z.string().max(1000, "Description too long").optional(),
startTime: z.string().datetime("Invalid start time").optional(),
endTime: z.string().datetime("Invalid end time").optional(),
status: z.nativeEnum(AppointmentStatus).optional(),
depositAmount: z.number().positive("Deposit must be positive").optional(),
totalAmount: z.number().positive("Total amount must be positive").optional(),
notes: z.string().max(1000, "Notes too long").optional(),
}).refine(
(data) => {
if (data.startTime && data.endTime) {
return new Date(data.endTime) > new Date(data.startTime)
}
return true
},
{
message: "End time must be after start time",
path: ["endTime"],
}
)
// Site settings validation schemas
export const socialMediaLinksSchema = z.object({
instagram: z.string().url("Invalid Instagram URL").optional(),
facebook: z.string().url("Invalid Facebook URL").optional(),
twitter: z.string().url("Invalid Twitter URL").optional(),
tiktok: z.string().url("Invalid TikTok URL").optional(),
})
export const businessHoursSchema = z.object({
dayOfWeek: z.number().int().min(0).max(6),
openTime: z.string().regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, "Invalid time format (HH:mm)"),
closeTime: z.string().regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, "Invalid time format (HH:mm)"),
isClosed: z.boolean().default(false),
})
export const siteSettingsSchema = z.object({
id: z.string().uuid(),
studioName: z.string().min(1, "Studio name is required"),
description: z.string().min(10, "Description must be at least 10 characters"),
address: z.string().min(5, "Address is required"),
phone: z.string().regex(/^[\+]?[1-9][\d]{0,15}$/, "Invalid phone number"),
email: z.string().email("Invalid email address"),
socialMedia: socialMediaLinksSchema,
businessHours: z.array(businessHoursSchema),
heroImage: z.string().url("Invalid hero image URL").optional(),
logoUrl: z.string().url("Invalid logo URL").optional(),
})
export const updateSiteSettingsSchema = z.object({
studioName: z.string().min(1, "Studio name is required").max(100, "Studio name too long").optional(),
description: z.string().min(10, "Description must be at least 10 characters").max(1000, "Description too long").optional(),
address: z.string().min(5, "Address is required").max(200, "Address too long").optional(),
phone: z.string().regex(/^[\+]?[1-9][\d]{0,15}$/, "Invalid phone number").optional(),
email: z.string().email("Invalid email address").optional(),
socialMedia: socialMediaLinksSchema.optional(),
businessHours: z.array(businessHoursSchema).optional(),
heroImage: z.string().url("Invalid hero image URL").optional(),
logoUrl: z.string().url("Invalid logo URL").optional(),
})
// File upload validation schemas
export const fileUploadSchema = z.object({
id: z.string().uuid(),
filename: z.string().min(1, "Filename is required"),
originalName: z.string().min(1, "Original name is required"),
mimeType: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_.]*$/, "Invalid MIME type"),
size: z.number().positive("File size must be positive"),
url: z.string().url("Invalid file URL"),
uploadedBy: z.string().uuid("Invalid user ID"),
})
export const createFileUploadSchema = z.object({
filename: z.string().min(1, "Filename is required"),
originalName: z.string().min(1, "Original name is required"),
mimeType: z.string().regex(/^image\/(jpeg|jpg|png|gif|webp)$/, "Only image files are allowed"),
size: z.number().positive("File size must be positive").max(10 * 1024 * 1024, "File too large (max 10MB)"),
uploadedBy: z.string().uuid("Invalid user ID"),
})
// Query parameter validation schemas
export const paginationSchema = z.object({
page: z.string().regex(/^\d+$/).transform(Number).pipe(z.number().int().min(1)).default("1"),
limit: z.string().regex(/^\d+$/).transform(Number).pipe(z.number().int().min(1).max(100)).default("10"),
})
export const artistFiltersSchema = z.object({
isActive: z.string().transform(val => val === "true").optional(),
specialty: z.string().optional(),
search: z.string().optional(),
})
export const appointmentFiltersSchema = z.object({
artistId: z.string().uuid().optional(),
clientId: z.string().uuid().optional(),
status: z.nativeEnum(AppointmentStatus).optional(),
startDate: z.string().datetime().optional(),
endDate: z.string().datetime().optional(),
})
// Form validation schemas (for react-hook-form)
export const loginFormSchema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(1, "Password is required"),
})
export const signupFormSchema = z.object({
name: z.string().min(1, "Name is required").max(100, "Name too long"),
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
confirmPassword: z.string().min(1, "Please confirm your password"),
}).refine(
(data) => data.password === data.confirmPassword,
{
message: "Passwords don't match",
path: ["confirmPassword"],
}
)
export const contactFormSchema = z.object({
name: z.string().min(1, "Name is required").max(100, "Name too long"),
email: z.string().email("Invalid email address"),
phone: z.string().regex(/^[\+]?[1-9][\d]{0,15}$/, "Invalid phone number").optional(),
subject: z.string().min(1, "Subject is required").max(200, "Subject too long"),
message: z.string().min(10, "Message must be at least 10 characters").max(1000, "Message too long"),
})
export const bookingFormSchema = z.object({
artistId: z.string().uuid("Please select an artist"),
name: z.string().min(1, "Name is required").max(100, "Name too long"),
email: z.string().email("Invalid email address"),
phone: z.string().regex(/^[\+]?[1-9][\d]{0,15}$/, "Invalid phone number"),
preferredDate: z.string().min(1, "Please select a preferred date"),
tattooDescription: z.string().min(10, "Please provide more details about your tattoo").max(1000, "Description too long"),
size: z.enum(["small", "medium", "large", "sleeve"], {
required_error: "Please select a size",
}),
placement: z.string().min(1, "Please specify placement").max(100, "Placement description too long"),
budget: z.string().optional(),
hasAllergies: z.boolean().default(false),
allergies: z.string().max(500, "Allergies description too long").optional(),
additionalNotes: z.string().max(500, "Additional notes too long").optional(),
})
// Type exports for form data
export type LoginFormData = z.infer<typeof loginFormSchema>
export type SignupFormData = z.infer<typeof signupFormSchema>
export type ContactFormData = z.infer<typeof contactFormSchema>
export type BookingFormData = z.infer<typeof bookingFormSchema>
export type CreateArtistData = z.infer<typeof createArtistSchema>
export type UpdateArtistData = z.infer<typeof updateArtistSchema>
export type CreatePortfolioImageData = z.infer<typeof createPortfolioImageSchema>
export type UpdatePortfolioImageData = z.infer<typeof updatePortfolioImageSchema>
export type CreateAppointmentData = z.infer<typeof createAppointmentSchema>
export type UpdateAppointmentData = z.infer<typeof updateAppointmentSchema>
export type UpdateSiteSettingsData = z.infer<typeof updateSiteSettingsSchema>