'use client' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import dynamic from 'next/dynamic' import { usePathname, useRouter, useSearchParams } from 'next/navigation' import { texts } from '@/texts' import type { PreviousAttendee } from '@/services/events' import { addToast } from '@/lib/toast' import WizardStepper from '@/components/events/create/WizardStepper' import Button from '@/components/formElements/Button' import { mapEventToWizardCloneData } from '@/components/events/create/cloneFromEvent' import { DetailSkeleton } from '@/components/feedback/LoadingState' import { combineDateAndMinutes, hasRequiredPosters, INITIAL_WIZARD_DATA, WIZARD_STEPS, type EventWizardFormData, } from '@/components/events/create/types' import { APP_ROUTES } from '@/constants/routes' import { createEvent, createEventFaq, fetchEventForEdit } from '@/services/events' import { fetchEventFaqs, fetchEventMedia } from '@/services/eventDetail' import { fetchAllCities, type City } from '@/services/geography' import { extractServerErrorDetail } from '@/services/errorHandler' import { GET_ME } from '@/services/users' import useAuth from '@/hooks/useAuth' import { clearEventDraft, loadEventDraft, saveEventDraft } from '@/features/events/eventDraftStore' import { usePublicCategoriesQuery } from '@/queries/consumer/usePublicCategoriesQuery' import { ANALYTICS_EVENTS, trackAnalyticsEvent } from '@/lib/analytics' const Step1BasicInfo = dynamic(() => import('@/components/events/create/steps/Step1BasicInfo'), { ssr: false, loading: () => , }) const Step2ScheduleLocation = dynamic(() => import('@/components/events/create/steps/Step2ScheduleLocation'), { ssr: false, loading: () => , }) const Step3PricingCapacity = dynamic(() => import('@/components/events/create/steps/Step3PricingCapacity'), { ssr: false, loading: () => , }) const Step4FaqPreview = dynamic(() => import('@/components/events/create/steps/Step4FaqPreview'), { ssr: false, loading: () => , }) const Step5GuestList = dynamic(() => import('@/components/events/create/steps/Step5GuestList'), { ssr: false, }) interface EventCreateWizardProps { finishRoute?: string } export default function EventCreateWizard({ finishRoute = APP_ROUTES.MANAGE_EVENTS }: EventCreateWizardProps = {}) { const router = useRouter() const pathname = usePathname() const searchParams = useSearchParams() const cloneFromIdRef = useRef(searchParams.get('cloneFrom')) const hasTrackedStartRef = useRef(false) const { user } = useAuth() const userId = user?.userId const [step, setStep] = useState(1) const [data, setData] = useState(INITIAL_WIZARD_DATA) const [isDraftHydrated, setIsDraftHydrated] = useState(false) const [wasDraftRestored, setWasDraftRestored] = useState(false) const [wasCloneLoaded, setWasCloneLoaded] = useState(false) const [isSaving, setIsSaving] = useState(false) const [createdEventId, setCreatedEventId] = useState(null) const [previousAttendees, setPreviousAttendees] = useState([]) const [showPostCreateInvite, setShowPostCreateInvite] = useState(false) useEffect(() => { if (!userId || hasTrackedStartRef.current) return hasTrackedStartRef.current = true trackAnalyticsEvent(ANALYTICS_EVENTS.EVENT_CREATE_STARTED, { creation_source: 'admin', is_clone: Boolean(cloneFromIdRef.current), }) }, [userId]) // Shared hook — same key/staleTime as Step1BasicInfo + CategoryBrowse. const categoriesQuery = usePublicCategoriesQuery() const categories = useMemo(() => categoriesQuery.data?.items ?? [], [categoriesQuery.data]) const [cities, setCities] = useState([]) useEffect(() => { if (!userId) return let cancelled = false const cloneFromId = cloneFromIdRef.current const hydrate = async () => { if (cloneFromId) { try { const [event, media, faqs] = await Promise.all([ fetchEventForEdit(cloneFromId), fetchEventMedia(cloneFromId).catch(() => []), fetchEventFaqs(cloneFromId).catch(() => []), ]) if (cancelled) return clearEventDraft(userId) setData(mapEventToWizardCloneData(event, media, faqs)) setStep(1) setWasCloneLoaded(true) setWasDraftRestored(false) setIsDraftHydrated(true) router.replace(pathname) return } catch { if (cancelled) return addToast({ title: texts.events.wizardCloneLoadFailed, color: 'danger' }) } } const stored = loadEventDraft(userId) if (stored) { setData({ ...INITIAL_WIZARD_DATA, ...stored.data }) setStep(stored.step) setWasDraftRestored(true) setIsDraftHydrated(true) return } // Fresh create: prefill host default venue from profile contact info. try { const [meResult, cityRows] = await Promise.all([GET_ME(), fetchAllCities()]) if (cancelled) return setCities(cityRows) if (meResult.ok) { const city = meResult.data.cityId ? cityRows.find((row) => row.id === meResult.data.cityId) : undefined setData({ ...INITIAL_WIZARD_DATA, ...(meResult.data.defaultAddress ? { address: meResult.data.defaultAddress } : {}), ...(city ? { cityId: String(city.id), provinceId: String(city.provinceId) } : {}), }) } } catch { // Prefill is best-effort; wizard still opens with empty location. } setIsDraftHydrated(true) } void hydrate() return () => { cancelled = true } // Hydrate once per userId. pathname/router are read from the render that starts hydration. // eslint-disable-next-line react-hooks/exhaustive-deps -- avoid re-cloning after URL cleanup }, [userId]) useEffect(() => { if (!userId || !isDraftHydrated || createdEventId) return const timeout = window.setTimeout(() => { saveEventDraft(userId, data, step) }, 250) return () => { window.clearTimeout(timeout) } }, [createdEventId, data, isDraftHydrated, step, userId]) useEffect(() => { // دریافت استان‌ها فعلاً غیرفعال است؛ نام شهر مستقیماً از فهرست شهرها خوانده می‌شود. // void fetchProvinces().then(setProvinces) void fetchAllCities().then(setCities) }, []) const patchData = useCallback((patch: Partial) => { setData((prev) => ({ ...prev, ...patch })) }, []) const categoryName = useMemo(() => categories.find((item) => String(item.id) === data.categoryId)?.name, [categories, data.categoryId]) // نام استان فعلاً در پیش‌نمایش رویداد نمایش داده نمی‌شود. // const provinceName = useMemo(() => provinces.find((item) => String(item.id) === data.provinceId)?.name, [provinces, data.provinceId]) const cityName = useMemo(() => cities.find((item) => String(item.id) === data.cityId)?.name, [cities, data.cityId]) const currentStepTitle = WIZARD_STEPS.find((item) => item.key === step)?.label const handleSaveDraft = async () => { if (!hasRequiredPosters(data.media)) { addToast({ title: texts.events.postersRequiredTitle, description: texts.events.postersRequiredDescription, color: 'danger', }) setStep(1) return } setIsSaving(true) let eventId: string | null = null try { const startsAt = combineDateAndMinutes(data.startDate, data.startTime) const endsAt = combineDateAndMinutes(data.endDate, data.endTime) if (new Date(endsAt) <= new Date(startsAt)) { addToast({ title: texts.validation.eventWizard.endAfterStart, color: 'danger' }) return } const event = await createEvent({ title: data.title, slug: data.slug, categoryId: Number(data.categoryId), shortDescription: data.shortDescription || undefined, description: data.description || undefined, startsAt, endsAt, provinceId: Number(data.provinceId), cityId: Number(data.cityId), address: data.address, lat: data.lat, lng: data.lng, isFree: data.isFree, price: data.isFree ? 0 : data.price, capacity: data.capacity, reservedCapacity: data.reservedCapacity, genderRestriction: data.genderRestriction, ageRestriction: data.ageRestriction, cancellationFeePercent: data.cancellationFeePercent, cancellationFeePercent12To24Hours: data.cancellationFeePercent12To24Hours, cancellationFeePercentMoreThan24Hours: data.cancellationFeePercentMoreThan24Hours, settings: { isDiscoverable: data.isDiscoverable, autoCreateGroup: data.autoCreateGroup, waitlistAutoOffer: data.waitlistAutoOffer, sendReviewRequestSms: data.sendReviewRequestSms, addressVisibility: data.addressVisibility, generalArea: data.addressVisibility === 'attendees_only' ? data.generalArea : undefined, }, media: data.media.map((item) => ({ mediaType: 'image' as const, url: item.url, sortOrder: item.sortOrder, isPoster: item.isPoster, isSquarePoster: item.isSquarePoster, })), }) eventId = event.id setCreatedEventId(event.id) if (userId) clearEventDraft(userId) for (let index = 0; index < data.faqs.length; index += 1) { const faq = data.faqs[index] await createEventFaq(event.id, { question: faq.question, answer: faq.answer, sortOrder: index, }) } addToast({ title: texts.events.draftSaved, color: 'success' }) const { loadPreviousAttendeesForStep5 } = await import('@/components/events/create/steps/Step5GuestList') const attendees = await loadPreviousAttendeesForStep5() if (attendees.length === 0) { router.push(finishRoute) return } setPreviousAttendees(attendees) setShowPostCreateInvite(true) } catch (error) { const detail = extractServerErrorDetail((error as { response?: { data?: unknown } })?.response?.data) if (eventId) { addToast({ title: texts.events.createdPartialTitle, description: detail ?? texts.events.createdPartialDescription, color: 'warning', }) router.push(APP_ROUTES.MANAGE_EVENT_DETAIL(eventId)) return } addToast({ title: texts.events.draftSaveFailed, description: detail ?? undefined, color: 'danger', }) } finally { setIsSaving(false) } } if (!userId || !isDraftHydrated) return return (
{wasCloneLoaded ? texts.events.wizardCloneHint : wasDraftRestored ? texts.events.wizardDraftRestoredHint : texts.events.wizardAutosaveHint}
{currentStepTitle ?

{currentStepTitle}

: null} {step === 1 && ( { patchData(patch) setStep(2) }} /> )} {step === 2 && ( { setStep(1) }} onChange={patchData} onNext={(patch) => { patchData(patch) setStep(3) }} /> )} {step === 3 && ( { setStep(2) }} onChange={patchData} onNext={(patch) => { patchData(patch) setStep(4) }} /> )} {step === 4 && ( { setStep(3) }} onChange={patchData} onSaveDraft={() => void handleSaveDraft()} /> )}
{createdEventId ? ( { router.push(finishRoute) }} onOpenChange={setShowPostCreateInvite} /> ) : null}
) }