Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
182 lines
5.4 KiB
TypeScript
182 lines
5.4 KiB
TypeScript
'use client'
|
|
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { useEffect, useMemo, useRef } from 'react'
|
|
import { FormProvider, useForm } from 'react-hook-form'
|
|
|
|
import { texts, format } from '@/texts'
|
|
import type { EventWizardEditLocks, EventWizardFormData } from '@/components/events/create/consumer/types'
|
|
import { hasRequiredPosters } from '@/components/events/create/consumer/types'
|
|
import { addToast } from '@/lib/toast'
|
|
import EventMediaGalleryUploader from '@/components/events/create/EventMediaGalleryUploader'
|
|
import { EventWizardStep1Validation, type EventWizardStep1Values } from '@/validation/eventWizard'
|
|
import { shouldAutogenerateEventSlug, slugifyEventTitle } from '@/features/events/eventSlug'
|
|
import { usePublicCategoriesQuery } from '@/queries/consumer/usePublicCategoriesQuery'
|
|
import { showFormValidationToast } from '@/lib/formValidationToast'
|
|
import ConsumerInput from '@/components/consumer/ConsumerInput'
|
|
import { buildCategoryTree } from '@/helpers/categoryPath'
|
|
|
|
interface Step1BasicInfoProps {
|
|
data: EventWizardFormData
|
|
/** Present in edit mode; media stays editable regardless of locks. */
|
|
locks?: EventWizardEditLocks
|
|
onBindSubmit: (submit: (() => void) | null) => void
|
|
onChange: (patch: Partial<EventWizardFormData>) => void
|
|
onNext: (patch: Partial<EventWizardFormData>) => void
|
|
}
|
|
|
|
export default function Step1BasicInfo({ data, onBindSubmit, onChange, onNext }: Step1BasicInfoProps) {
|
|
const categoriesQuery = usePublicCategoriesQuery()
|
|
const categoryOptions = useMemo(
|
|
() =>
|
|
buildCategoryTree(categoriesQuery.data?.items ?? []).map((option) => ({
|
|
depth: option.depth,
|
|
id: String(option.id),
|
|
name: option.name,
|
|
})),
|
|
[categoriesQuery.data?.items]
|
|
)
|
|
const form = useForm<EventWizardStep1Values>({
|
|
resolver: zodResolver(EventWizardStep1Validation),
|
|
defaultValues: {
|
|
title: data.title,
|
|
slug: data.slug,
|
|
categoryId: data.categoryId,
|
|
shortDescription: data.shortDescription,
|
|
description: data.description,
|
|
},
|
|
})
|
|
|
|
const followTitleRef = useRef(shouldAutogenerateEventSlug(data.slug, data.title))
|
|
const lastAutoSlugRef = useRef(data.slug || slugifyEventTitle(data.title))
|
|
const title = form.watch('title')
|
|
const slug = form.watch('slug')
|
|
|
|
useEffect(() => {
|
|
if (categoriesQuery.isError) {
|
|
addToast({ title: texts.events.categoriesLoadFailed, color: 'danger' })
|
|
}
|
|
}, [categoriesQuery.isError])
|
|
|
|
useEffect(() => {
|
|
if (!followTitleRef.current) return
|
|
|
|
const next = slugifyEventTitle(title ?? '')
|
|
|
|
lastAutoSlugRef.current = next
|
|
if (form.getValues('slug') !== next) {
|
|
form.setValue('slug', next)
|
|
}
|
|
}, [form, title])
|
|
|
|
useEffect(() => {
|
|
const subscription = form.watch((values, info) => {
|
|
if (info.name === 'slug' && (values.slug ?? '') !== lastAutoSlugRef.current) {
|
|
followTitleRef.current = false
|
|
}
|
|
|
|
onChange({
|
|
title: values.title ?? '',
|
|
slug: values.slug ?? '',
|
|
categoryId: values.categoryId ?? '',
|
|
shortDescription: values.shortDescription ?? '',
|
|
description: values.description ?? '',
|
|
})
|
|
})
|
|
|
|
return () => {
|
|
subscription.unsubscribe()
|
|
}
|
|
}, [form, onChange])
|
|
|
|
const handleSubmit = form.handleSubmit((values) => {
|
|
if (!hasRequiredPosters(data.media)) {
|
|
addToast({
|
|
title: texts.events.postersRequiredTitle,
|
|
description: texts.events.postersRequiredShort,
|
|
color: 'danger',
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
onNext({
|
|
title: values.title,
|
|
slug: values.slug,
|
|
categoryId: values.categoryId,
|
|
shortDescription: values.shortDescription ?? '',
|
|
description: values.description ?? '',
|
|
media: data.media,
|
|
})
|
|
}, showFormValidationToast)
|
|
|
|
useEffect(() => {
|
|
onBindSubmit(() => {
|
|
void handleSubmit()
|
|
})
|
|
|
|
return () => {
|
|
onBindSubmit(null)
|
|
}
|
|
}, [handleSubmit, onBindSubmit])
|
|
|
|
return (
|
|
<FormProvider {...form}>
|
|
<form
|
|
className="consumer-surface flex flex-col gap-4 p-4"
|
|
onSubmit={(event) => {
|
|
event.preventDefault()
|
|
void handleSubmit()
|
|
}}
|
|
>
|
|
<ConsumerInput
|
|
generalType="input"
|
|
label={texts.events.eventTitleLabel}
|
|
maxValue={200}
|
|
name="title"
|
|
/>
|
|
|
|
<ConsumerInput
|
|
generalType="select"
|
|
label={texts.common.category}
|
|
name="categoryId"
|
|
placeholder={texts.events.categoryPlaceholder}
|
|
selectKey="id"
|
|
selectOptions={categoryOptions}
|
|
selectValue="name"
|
|
/>
|
|
|
|
<EventMediaGalleryUploader
|
|
items={data.media}
|
|
onChange={(media) => {
|
|
onChange({ media })
|
|
}}
|
|
/>
|
|
|
|
<ConsumerInput
|
|
generalType="textarea"
|
|
label={texts.events.shortDescriptionLabel}
|
|
name="shortDescription"
|
|
textAreaMinRows={2}
|
|
/>
|
|
|
|
<ConsumerInput
|
|
generalType="textarea"
|
|
label={texts.events.fullDescriptionLabel}
|
|
name="description"
|
|
textAreaMinRows={5}
|
|
/>
|
|
|
|
<ConsumerInput
|
|
description={slug ? format(texts.events.slugPublicLink, { slug }) : texts.events.slugHintFromTitle}
|
|
direction="ltr"
|
|
generalType="input"
|
|
label={texts.events.slugLabel}
|
|
maxValue={220}
|
|
name="slug"
|
|
/>
|
|
</form>
|
|
</FormProvider>
|
|
)
|
|
}
|