159 lines
5.4 KiB
TypeScript
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>
|