admin/validation/eventWizard.ts

159 lines
5.4 KiB
TypeScript

import { z } from 'zod'
import { EVENT_SLUG_MAX_LENGTH } from '@/features/events/eventSlug'
import { isPublicEventSlug } from '@/lib/routing/eventRoute'
import { texts } from '@/texts'
// هم‌تراز با اسلاگ دسته‌بندی‌ها — فقط لاتین، بدون تبدیل ارقام فارسی
const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
const v = texts.validation.eventWizard
/** ترکیب تاریخ ISO و دقیقهٔ روز برای مقایسهٔ شروع/پایان (بدون وابستگی به components) */
function toDateTime(dateIso: string, minutes: number): Date | null {
const date = new Date(dateIso)
if (!Number.isFinite(date.getTime()) || !Number.isFinite(minutes)) return null
date.setHours(Math.floor(minutes / 60), minutes % 60, 0, 0)
return date
}
export const EventWizardStep1Validation = z.object({
title: z.string().trim().min(1, v.titleRequired).max(200, v.titleMax),
slug: z
.string()
.trim()
.min(1, v.slugRequired)
.max(EVENT_SLUG_MAX_LENGTH, v.slugMax)
.regex(SLUG_PATTERN, v.slugPattern)
.refine(isPublicEventSlug, v.slugReserved),
categoryId: z.string().min(1, v.categoryRequired),
shortDescription: z.string().trim().max(500, v.shortDescriptionMax).optional().or(z.literal('')),
description: z.string().optional().or(z.literal('')),
})
export type EventWizardStep1Values = z.infer<typeof EventWizardStep1Validation>
const eventWizardStep2ScheduleRefine = (
values: {
startDate: string
startTime: number
endDate: string
endTime: number
addressVisibility: 'public' | 'attendees_only'
generalArea?: string
},
ctx: z.RefinementCtx
) => {
const startsAt = toDateTime(values.startDate, values.startTime)
const endsAt = toDateTime(values.endDate, values.endTime)
if (!startsAt || !endsAt) {
ctx.addIssue({
code: 'custom',
message: v.startEndRequired,
path: ['startDate'],
})
return
}
if (endsAt <= startsAt) {
ctx.addIssue({
code: 'custom',
message: v.endAfterStart,
path: ['endDate'],
})
}
if (values.addressVisibility === 'attendees_only' && !values.generalArea?.trim()) {
ctx.addIssue({
code: 'custom',
message: v.generalAreaRequired,
path: ['generalArea'],
})
}
}
const eventWizardStep2Fields = z.object({
startDate: z.string().min(1, v.startDateRequired),
startTime: z.number({ error: v.startTimeRequired }),
endDate: z.string().min(1, v.endDateRequired),
endTime: z.number({ error: v.endTimeRequired }),
provinceId: z.string().optional().or(z.literal('')),
cityId: z.string().min(1, texts.validation.cityRequired),
address: z.string().trim().min(1, v.addressRequired),
addressVisibility: z.enum(['public', 'attendees_only']),
generalArea: z.string().trim().optional().or(z.literal('')),
})
export const EventWizardStep2Validation = eventWizardStep2Fields.superRefine(eventWizardStep2ScheduleRefine)
export type EventWizardStep2Values = z.infer<typeof EventWizardStep2Validation>
export {
ConsumerEventWizardStep2Validation,
parseConsumerEventWizardStep2,
type ConsumerEventWizardStep2FieldErrors,
type ConsumerEventWizardStep2Values,
} from '@/validation/consumerEventWizardStep2'
export const EventWizardStep3Validation = z
.object({
isFree: z.boolean(),
price: z.number({ error: v.priceRequired }).min(0, v.priceNonNegative),
capacity: z.number({ error: v.capacityRequired }).int(v.capacityInteger).min(1, v.capacityMin),
reservedCapacity: z.number({ error: v.reservedCapacityRequired }).int(v.reservedCapacityInteger).min(0, v.reservedCapacityNonNegative),
genderRestriction: z.enum(['open', 'female_only', 'male_only']),
ageRestriction: z.enum(['open', 'age_15_19', 'age_20_24', 'age_25_plus']),
cancellationFeePercent: z.number({ error: v.cancelPenaltyRequired }).min(0, v.cancelPenaltyNonNegative).max(100, v.cancelPenaltyMax),
cancellationFeePercent12To24Hours: z
.number({ error: v.cancelPenaltyRequired })
.min(0, v.cancelPenaltyNonNegative)
.max(100, v.cancelPenaltyMax),
cancellationFeePercentMoreThan24Hours: z
.number({ error: v.cancelPenaltyRequired })
.min(0, v.cancelPenaltyNonNegative)
.max(100, v.cancelPenaltyMax),
isDiscoverable: z.boolean(),
autoCreateGroup: z.boolean(),
waitlistAutoOffer: z.boolean(),
sendReviewRequestSms: z.boolean(),
})
.superRefine((values, ctx) => {
if (!values.isFree && values.price <= 0) {
ctx.addIssue({
code: 'custom',
message: v.paidPricePositive,
path: ['price'],
})
}
if (values.reservedCapacity > values.capacity) {
ctx.addIssue({
code: 'custom',
message: v.reservedExceedsCapacity,
path: ['reservedCapacity'],
})
}
if (
values.cancellationFeePercent < values.cancellationFeePercent12To24Hours ||
values.cancellationFeePercent12To24Hours < values.cancellationFeePercentMoreThan24Hours
) {
ctx.addIssue({
code: 'custom',
message: v.cancelPenaltyOrder,
path: ['cancellationFeePercent12To24Hours'],
})
}
})
export type EventWizardStep3Values = z.infer<typeof EventWizardStep3Validation>
export const EventWizardFaqFormValidation = z.object({
question: z.string().trim().min(1, v.faqQuestionRequired),
answer: z.string().trim().min(1, v.faqAnswerRequired),
})
export type EventWizardFaqFormValues = z.infer<typeof EventWizardFaqFormValidation>