admin/components/events/create/consumer/steps/Step2ScheduleLocation.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

222 lines
7.5 KiB
TypeScript

'use client'
import dynamic from 'next/dynamic'
import { useCallback, useEffect, useState } from 'react'
import { texts } from '@/texts'
import type { EventWizardEditLocks, EventWizardFormData } from '@/components/events/create/consumer/types'
import ConsumerInput from '@/components/consumer/ConsumerInput'
import InlineNotice from '@/components/feedback/InlineNotice'
import MapLocationPicker from '@/components/events/create/MapLocationPicker'
import { useEventScheduleConstraints } from '@/components/events/schedule/eventScheduleConstraints'
import { parseConsumerEventWizardStep2, type ConsumerEventWizardStep2FieldErrors } from '@/validation/consumerEventWizardStep2'
import { addToast } from '@/lib/toast'
const EventScheduleFieldControl = dynamic(() => import('@/components/events/schedule/EventScheduleFieldControl'), {
ssr: false,
})
interface Step2ScheduleLocationProps {
data: EventWizardFormData
locks?: EventWizardEditLocks
onBindSubmit: (submit: (() => void) | null) => void
onChange: (patch: Partial<EventWizardFormData>) => void
onNext: (patch: Partial<EventWizardFormData>) => void
}
export default function Step2ScheduleLocation({ data, locks, onBindSubmit, onChange, onNext }: Step2ScheduleLocationProps) {
const [errors, setErrors] = useState<ConsumerEventWizardStep2FieldErrors>({})
const { todayIso, startMinimumTime, endMinimumTime } = useEventScheduleConstraints(data.startDate, data.startTime, data.endDate)
const scheduleLocked = Boolean(locks?.schedule)
const locationLocked = Boolean(locks?.location)
const showLockNotice = Boolean(locks?.notice && (scheduleLocked || locationLocked))
const patch = (next: Partial<EventWizardFormData>) => {
setErrors({})
onChange(next)
}
const handleSubmit = useCallback(() => {
const result = parseConsumerEventWizardStep2({
startDate: data.startDate,
startTime: data.startTime,
endDate: data.endDate,
endTime: data.endTime,
address: data.address,
addressVisibility: data.addressVisibility,
generalArea: data.addressVisibility === 'attendees_only' ? data.generalArea : '',
})
if (!result.success) {
setErrors(result.fieldErrors)
addToast({
title: texts.common.formIncomplete,
description: Object.values(result.fieldErrors).find(Boolean) ?? texts.common.formValidationDefault,
color: 'danger',
})
return
}
setErrors({})
onNext({
...result.data,
lat: data.lat,
lng: data.lng,
generalArea: result.data.addressVisibility === 'attendees_only' ? (result.data.generalArea?.trim() ?? '') : '',
})
}, [data, onNext])
useEffect(() => {
onBindSubmit(handleSubmit)
return () => {
onBindSubmit(null)
}
}, [handleSubmit, onBindSubmit])
return (
<form
className="consumer-surface flex flex-col gap-4 p-4"
onSubmit={(event) => {
event.preventDefault()
handleSubmit()
}}
>
{showLockNotice ? <InlineNotice>{locks?.notice}</InlineNotice> : null}
<div className="grid grid-cols-2 gap-2">
<div className="col-span-2">
<span className="label mb-1 block text-sm font-medium !text-secondary-20">{texts.events.scheduleSectionTitle}</span>
<span className="label mb-1 block text-sm font-normal !text-secondary-20">{texts.events.scheduleSectionDescription}</span>
</div>
<EventScheduleFieldControl
disabled={scheduleLocked}
errorMessage={errors.startDate}
kind="eventDatePicker"
label={texts.events.startsAtDateLabel}
minDate={todayIso}
triggerStyle="consumer"
value={data.startDate}
variant="flat"
onValueChange={(value) => {
if (scheduleLocked) return
patch({ startDate: String(value) })
}}
/>
<EventScheduleFieldControl
disabled={scheduleLocked}
errorMessage={errors.startTime}
kind="eventTimePicker"
label={texts.events.startsAtTimeLabel}
minValue={startMinimumTime}
triggerStyle="consumer"
value={data.startTime}
variant="flat"
onValueChange={(value) => {
if (scheduleLocked) return
patch({ startTime: Number(value) })
}}
/>
<EventScheduleFieldControl
disabled={scheduleLocked}
errorMessage={errors.endDate}
kind="eventDatePicker"
label={texts.events.endsAtDateLabel}
minDate={data.startDate || todayIso}
triggerStyle="consumer"
value={data.endDate}
variant="flat"
onValueChange={(value) => {
if (scheduleLocked) return
patch({ endDate: String(value) })
}}
/>
<EventScheduleFieldControl
disabled={scheduleLocked}
errorMessage={errors.endTime}
kind="eventTimePicker"
label={texts.events.endsAtTimeLabel}
minValue={endMinimumTime}
triggerStyle="consumer"
value={data.endTime}
variant="flat"
onValueChange={(value) => {
if (scheduleLocked) return
patch({ endTime: Number(value) })
}}
/>
</div>
<div className="flex flex-col gap-1">
<ConsumerInput
description={texts.events.addressHostContactHint}
disabled={locationLocked}
generalType="textarea"
label={texts.events.address}
name="address"
placeholder={texts.events.addressPlaceholder}
textAreaMinRows={3}
value={data.address}
onValueChange={(value) => {
if (locationLocked) return
patch({ address: typeof value === 'string' ? value : '' })
}}
/>
{errors.address ? <p className="text-sm text-fourth-900">{errors.address}</p> : null}
</div>
<MapLocationPicker
disabled={locationLocked}
displayAddress={data.address}
lat={data.lat}
lng={data.lng}
mode="modal"
onAddressResolved={(address) => {
if (locationLocked) return
patch({ address })
}}
onChange={({ lat, lng }) => {
if (locationLocked) return
patch({ lat, lng })
}}
/>
<ConsumerInput
description={texts.events.addressVisibilitySectionDescription}
disabled={locationLocked}
generalType="checkbox"
label={texts.events.addressVisibilityAttendeesOnly}
name="addressVisibilityAttendeesOnly"
value={data.addressVisibility === 'attendees_only'}
onValueChange={(checked) => {
if (locationLocked) return
patch({
addressVisibility: checked ? 'attendees_only' : 'public',
generalArea: checked ? data.generalArea : '',
})
}}
/>
{data.addressVisibility === 'attendees_only' ? (
<div className="flex flex-col gap-1">
<ConsumerInput
disabled={locationLocked}
generalType="textarea"
label={texts.events.generalAreaLabel}
name="generalArea"
placeholder={texts.events.generalAreaPlaceholder}
textAreaMinRows={2}
value={data.generalArea}
onValueChange={(value) => {
if (locationLocked) return
patch({ generalArea: typeof value === 'string' ? value : '' })
}}
/>
{errors.generalArea ? <p className="text-sm text-fourth-900">{errors.generalArea}</p> : null}
</div>
) : null}
</form>
)
}