Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
222 lines
7.5 KiB
TypeScript
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>
|
|
)
|
|
}
|