admin/components/events/create/consumer/steps/Step1BasicInfo.tsx
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

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