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 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 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 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