- Replaced `LIST_PUBLIC_CATEGORIES` with `LIST_ADMIN_CATEGORIES_FLAT` in `ArticleFormModal.tsx`. - Removed the "Add Event" button from the dashboard page. - Simplified the `EventsPage` by removing the button and adjusting the layout. - Cleaned up the `AdminEventEditPage` by removing unnecessary props. - Deleted unused layout and page files related to event creation. - Updated `AdminAuthContent` to use `useAdminCitiesQuery` instead of `useDiscoveryCitiesQuery`. - Refactored `EventGuestListAccessPanel` to remove the `accessMode` prop and adjust API calls accordingly. - Removed several unused components and tests related to event creation, enhancing project maintainability.
682 lines
23 KiB
TypeScript
682 lines
23 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
import { FormProvider, useForm } from 'react-hook-form'
|
|
import { useRouter } from 'next/navigation'
|
|
import dynamic from 'next/dynamic'
|
|
|
|
import { texts, format } from '@/texts'
|
|
import type { CreateEventPayload, CreatedEvent } from '@/services/events'
|
|
import Button from '@/components/formElements/Button'
|
|
import Input from '@/components/formElements/Input'
|
|
import CategoryTreeSelect from '@/components/events/create/CategoryTreeSelect'
|
|
import EventMediaGalleryUploader from '@/components/events/create/EventMediaGalleryUploader'
|
|
import FaqEditor from '@/components/events/create/FaqEditor'
|
|
import MapLocationPicker from '@/components/events/create/MapLocationPicker'
|
|
import { WizardFormFullWidth, WizardFormGrid } from '@/components/events/create/WizardFormLayout'
|
|
import type { FaqItem, StagedMediaItem } from '@/components/events/create/types'
|
|
import { combineDateAndMinutes, hasRequiredPosters } from '@/components/events/create/types'
|
|
import { DetailSkeleton } from '@/components/feedback/LoadingState'
|
|
import InlineNotice from '@/components/feedback/InlineNotice'
|
|
import { AdminFormActions, AdminFormSection } from '@/components/forms/AdminFormLayout'
|
|
import UnsavedChangesIndicator from '@/components/forms/UnsavedChangesIndicator'
|
|
import { useEventScheduleConstraints } from '@/components/events/schedule/eventScheduleConstraints'
|
|
import { isEventSlugLocked, isFieldLocked, resolveEventEditPolicy } from '@/features/events/eventEditPolicy'
|
|
import { syncEventFaqs, syncEventMedia } from '@/features/events/edit/syncEventExtras'
|
|
import {
|
|
fetchEventCategories,
|
|
fetchEventForEdit,
|
|
updateEventAsAdmin,
|
|
type EventCategory,
|
|
} from '@/services/events'
|
|
import { fetchEventFaqs, fetchEventMedia, type EventFaq, type EventMedia } from '@/services/eventDetail'
|
|
import { fetchAllCities, type City } from '@/services/geography'
|
|
import { extractServerErrorDetail } from '@/services/errorHandler'
|
|
import { addToast } from '@/lib/toast'
|
|
import { showFormValidationToast } from '@/lib/formValidationToast'
|
|
import { AGE_RESTRICTION_OPTIONS, GENDER_RESTRICTION_OPTIONS } from '@/lib/eventAudience'
|
|
|
|
const TextEditor = dynamic(() => import('@/components/formElements/TextEditor'), { ssr: false })
|
|
|
|
interface EventEditFormValues {
|
|
title: string
|
|
slug: string
|
|
categoryId: string
|
|
shortDescription: string
|
|
description: string
|
|
startDate: string
|
|
startTime: number
|
|
endDate: string
|
|
endTime: number
|
|
cityId: string
|
|
address: string
|
|
lat: number
|
|
lng: number
|
|
isFree: boolean
|
|
price: number
|
|
capacity: number
|
|
genderRestriction: 'open' | 'female_only' | 'male_only'
|
|
ageRestriction: 'open' | 'age_15_19' | 'age_20_24' | 'age_25_plus'
|
|
cancellationFeePercent: number
|
|
cancellationFeePercent12To24Hours: number
|
|
cancellationFeePercentMoreThan24Hours: number
|
|
isDiscoverable: boolean
|
|
autoCreateGroup: boolean
|
|
addressVisibility: 'public' | 'attendees_only'
|
|
generalArea: string
|
|
waitlistAutoOffer: boolean
|
|
sendReviewRequestSms: boolean
|
|
}
|
|
|
|
const ADDRESS_VISIBILITY_OPTIONS = [
|
|
{ name: texts.events.addressVisibilityPublic, selectKey: 'public' },
|
|
{ name: texts.events.addressVisibilityAttendeesOnly, selectKey: 'attendees_only' },
|
|
]
|
|
|
|
interface EventEditFormProps {
|
|
eventId: string
|
|
cancelHref: string
|
|
successHref: string
|
|
}
|
|
|
|
function splitIsoToDateAndMinutes(iso: string): { date: string; minutes: number } {
|
|
const d = new Date(iso)
|
|
|
|
return {
|
|
date: d.toISOString(),
|
|
minutes: d.getHours() * 60 + d.getMinutes(),
|
|
}
|
|
}
|
|
|
|
function eventToFormValues(event: CreatedEvent): EventEditFormValues {
|
|
const start = splitIsoToDateAndMinutes(String(event.startsAt))
|
|
const end = splitIsoToDateAndMinutes(String(event.endsAt))
|
|
|
|
return {
|
|
title: event.title,
|
|
slug: event.slug,
|
|
categoryId: String(event.categoryId),
|
|
shortDescription: event.shortDescription ?? '',
|
|
description: event.description ?? '',
|
|
startDate: start.date,
|
|
startTime: start.minutes,
|
|
endDate: end.date,
|
|
endTime: end.minutes,
|
|
cityId: String(event.cityId),
|
|
// The editing party (owner or admin) always gets the real address from
|
|
// the backend (see EventsService.applyAddressVisibility) -- the `?? `
|
|
// fallbacks only satisfy the DTO's general nullable type.
|
|
address: event.address ?? '',
|
|
lat: event.lat ?? 35.6892,
|
|
lng: event.lng ?? 51.389,
|
|
isFree: event.isFree,
|
|
price: event.price,
|
|
capacity: event.capacity,
|
|
genderRestriction: event.genderRestriction ?? 'open',
|
|
ageRestriction: event.ageRestriction ?? 'open',
|
|
cancellationFeePercent: event.cancellationFeePercent,
|
|
cancellationFeePercent12To24Hours: event.cancellationFeePercent12To24Hours,
|
|
cancellationFeePercentMoreThan24Hours: event.cancellationFeePercentMoreThan24Hours,
|
|
isDiscoverable: event.settings.isDiscoverable,
|
|
autoCreateGroup: event.settings.autoCreateGroup,
|
|
addressVisibility: event.settings.addressVisibility,
|
|
generalArea: event.settings.generalArea ?? '',
|
|
waitlistAutoOffer: event.settings.waitlistAutoOffer,
|
|
sendReviewRequestSms: event.settings.sendReviewRequestSms ?? false,
|
|
}
|
|
}
|
|
|
|
const EventEditForm = ({ eventId, cancelHref, successHref }: EventEditFormProps) => {
|
|
const router = useRouter()
|
|
const FormActions = AdminFormActions
|
|
const FormSection = AdminFormSection
|
|
const [event, setEvent] = useState<CreatedEvent | null>(null)
|
|
const [categories, setCategories] = useState<EventCategory[]>([])
|
|
const [cities, setCities] = useState<City[]>([])
|
|
const [initialMedia, setInitialMedia] = useState<EventMedia[]>([])
|
|
const [initialFaqs, setInitialFaqs] = useState<EventFaq[]>([])
|
|
const [media, setMedia] = useState<StagedMediaItem[]>([])
|
|
const [faqs, setFaqs] = useState<FaqItem[]>([])
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [isSaving, setIsSaving] = useState(false)
|
|
const [loadError, setLoadError] = useState<string | null>(null)
|
|
|
|
const form = useForm<EventEditFormValues>()
|
|
const isFree = form.watch('isFree')
|
|
const lat = form.watch('lat')
|
|
const lng = form.watch('lng')
|
|
const addressVisibility = form.watch('addressVisibility')
|
|
const startDate = form.watch('startDate')
|
|
const startTime = form.watch('startTime')
|
|
const endDate = form.watch('endDate')
|
|
const { todayIso, startMinimumTime, endMinimumTime } = useEventScheduleConstraints(startDate, startTime, endDate)
|
|
|
|
const policy = useMemo(
|
|
() => (event ? resolveEventEditPolicy(event.status, event.bookedCount, event.reservedCapacity ?? 0) : null),
|
|
[event]
|
|
)
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
|
|
const load = async () => {
|
|
try {
|
|
setIsLoading(true)
|
|
setLoadError(null)
|
|
const [loaded, cats, cityList, mediaItems, faqItems] = await Promise.all([
|
|
fetchEventForEdit(eventId),
|
|
fetchEventCategories(),
|
|
fetchAllCities(),
|
|
fetchEventMedia(eventId).catch(() => [] as EventMedia[]),
|
|
fetchEventFaqs(eventId).catch(() => [] as EventFaq[]),
|
|
])
|
|
|
|
if (cancelled) return
|
|
setEvent(loaded)
|
|
setCategories(cats)
|
|
setCities(cityList)
|
|
setInitialMedia(mediaItems)
|
|
setInitialFaqs(faqItems)
|
|
setMedia(
|
|
mediaItems
|
|
.filter((item) => item.mediaType === 'image')
|
|
.map((item, index) => ({
|
|
id: item.id,
|
|
url: item.url,
|
|
isPoster: item.isPoster,
|
|
isSquarePoster: Boolean(item.isSquarePoster),
|
|
sortOrder: item.sortOrder ?? index,
|
|
}))
|
|
)
|
|
setFaqs(
|
|
faqItems.map((faq) => ({
|
|
id: faq.id,
|
|
question: faq.question,
|
|
answer: faq.answer,
|
|
}))
|
|
)
|
|
form.reset(eventToFormValues(loaded))
|
|
} catch {
|
|
if (!cancelled) setLoadError(texts.events.eventLoadFailed)
|
|
} finally {
|
|
if (!cancelled) setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
void load()
|
|
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [eventId, form])
|
|
|
|
const scheduleLocked = policy ? isFieldLocked(policy, 'startsAt') : false
|
|
const locationLocked = policy ? isFieldLocked(policy, 'address') : false
|
|
const priceLocked = policy ? isFieldLocked(policy, 'price') : false
|
|
const cancellationFeeLocked = policy ? isFieldLocked(policy, 'cancellationFeePercent') : false
|
|
const audienceLocked = policy ? isFieldLocked(policy, 'genderRestriction') : false
|
|
const behavioralLocked = policy?.lockBehavioralSettings ?? false
|
|
const slugLocked = event ? isEventSlugLocked(event.status) : false
|
|
|
|
const handleSubmit = form.handleSubmit(async (values) => {
|
|
if (!event || !policy?.canEdit) return
|
|
|
|
try {
|
|
const startsAt = combineDateAndMinutes(values.startDate, values.startTime)
|
|
const endsAt = combineDateAndMinutes(values.endDate, values.endTime)
|
|
|
|
if (new Date(endsAt) <= new Date(startsAt)) {
|
|
addToast({ title: texts.validation.eventWizard.endAfterStart, color: 'danger' })
|
|
|
|
return
|
|
}
|
|
|
|
if (!hasRequiredPosters(media)) {
|
|
addToast({
|
|
title: texts.events.postersRequiredTitle,
|
|
description: texts.events.postersRequiredDescription,
|
|
color: 'danger',
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
const selectedCity = cities.find((city) => String(city.id) === values.cityId)
|
|
|
|
if (!selectedCity) {
|
|
addToast({ title: texts.validation.cityRequired, color: 'danger' })
|
|
|
|
return
|
|
}
|
|
|
|
if (policy && values.capacity < policy.minCapacity) {
|
|
addToast({
|
|
title: format(texts.events.capacityBelowOccupied, { min: policy.minCapacity }),
|
|
color: 'danger',
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
if (
|
|
values.cancellationFeePercent < values.cancellationFeePercent12To24Hours ||
|
|
values.cancellationFeePercent12To24Hours < values.cancellationFeePercentMoreThan24Hours
|
|
) {
|
|
addToast({ title: texts.validation.eventWizard.cancelPenaltyOrder, color: 'danger' })
|
|
|
|
return
|
|
}
|
|
|
|
const price = values.isFree ? 0 : Number(values.price)
|
|
const payload: Partial<CreateEventPayload> = {
|
|
title: values.title.trim(),
|
|
categoryId: Number(values.categoryId),
|
|
shortDescription: values.shortDescription.trim() || undefined,
|
|
description: values.description || undefined,
|
|
capacity: Number(values.capacity),
|
|
}
|
|
|
|
if (!behavioralLocked) {
|
|
payload.settings = {
|
|
isDiscoverable: values.isDiscoverable,
|
|
autoCreateGroup: values.autoCreateGroup,
|
|
waitlistAutoOffer: values.waitlistAutoOffer,
|
|
sendReviewRequestSms: values.sendReviewRequestSms,
|
|
}
|
|
}
|
|
|
|
if (!slugLocked) {
|
|
payload.slug = values.slug.trim()
|
|
}
|
|
|
|
if (!scheduleLocked) {
|
|
payload.startsAt = startsAt
|
|
payload.endsAt = endsAt
|
|
}
|
|
|
|
if (!locationLocked) {
|
|
payload.provinceId = selectedCity.provinceId
|
|
payload.cityId = Number(values.cityId)
|
|
payload.address = values.address.trim()
|
|
payload.lat = values.lat
|
|
payload.lng = values.lng
|
|
payload.settings = {
|
|
...payload.settings,
|
|
addressVisibility: values.addressVisibility,
|
|
generalArea: values.addressVisibility === 'attendees_only' ? values.generalArea.trim() : undefined,
|
|
}
|
|
}
|
|
|
|
if (!priceLocked) {
|
|
payload.isFree = values.isFree
|
|
payload.price = price
|
|
}
|
|
|
|
if (!cancellationFeeLocked) {
|
|
payload.cancellationFeePercent = Number(values.cancellationFeePercent ?? 0)
|
|
payload.cancellationFeePercent12To24Hours = Number(values.cancellationFeePercent12To24Hours ?? 0)
|
|
payload.cancellationFeePercentMoreThan24Hours = Number(values.cancellationFeePercentMoreThan24Hours ?? 0)
|
|
}
|
|
|
|
if (!audienceLocked) {
|
|
payload.genderRestriction = values.genderRestriction
|
|
payload.ageRestriction = values.ageRestriction
|
|
}
|
|
|
|
setIsSaving(true)
|
|
const updated = await updateEventAsAdmin(eventId, payload)
|
|
|
|
await syncEventMedia(eventId, initialMedia, media)
|
|
await syncEventFaqs(eventId, initialFaqs, faqs)
|
|
|
|
setEvent(updated)
|
|
addToast({ title: texts.events.eventUpdated, color: 'success' })
|
|
router.push(successHref)
|
|
} catch (err) {
|
|
const detail = extractServerErrorDetail((err as { response?: { data?: unknown } })?.response?.data)
|
|
|
|
addToast({
|
|
title: texts.events.eventUpdateFailed,
|
|
description: detail ?? undefined,
|
|
color: 'danger',
|
|
})
|
|
} finally {
|
|
setIsSaving(false)
|
|
}
|
|
}, showFormValidationToast)
|
|
|
|
if (isLoading) return <DetailSkeleton />
|
|
if (loadError || !event || !policy) {
|
|
return <p className="text-sm text-fourth-900">{loadError ?? texts.events.eventNotFound}</p>
|
|
}
|
|
|
|
if (!policy.canEdit) {
|
|
return (
|
|
<div className="grid max-w-xl gap-3">
|
|
<p className="text-sm text-default-600">{format(texts.events.editStatusBlocked, { status: event.status })}</p>
|
|
<Button
|
|
size="sm"
|
|
variant="flat"
|
|
onClick={() => {
|
|
router.push(cancelHref)
|
|
}}
|
|
>
|
|
{texts.common.back}
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<FormProvider {...form}>
|
|
<form
|
|
className="mx-auto flex max-w-5xl flex-col gap-5"
|
|
onSubmit={(e) => void handleSubmit(e)}
|
|
>
|
|
{policy.hasActiveBookings ? (
|
|
<InlineNotice>{format(texts.events.editLockedWithBookings, { count: event.bookedCount })}</InlineNotice>
|
|
) : null}
|
|
|
|
<FormSection
|
|
description={texts.events.basicInfoCardDescription}
|
|
title={texts.events.basicInfoCardTitle}
|
|
>
|
|
<WizardFormGrid>
|
|
<Input
|
|
required
|
|
generalType="input"
|
|
label={texts.events.title}
|
|
name="title"
|
|
/>
|
|
<Input
|
|
required
|
|
description={slugLocked ? texts.events.slugLockedHint : texts.events.slugFormatHint}
|
|
direction="ltr"
|
|
disabled={slugLocked}
|
|
generalType="input"
|
|
label={texts.events.slugFieldLabel}
|
|
name="slug"
|
|
/>
|
|
<WizardFormFullWidth>
|
|
<CategoryTreeSelect
|
|
required
|
|
categories={categories}
|
|
label={texts.common.category}
|
|
name="categoryId"
|
|
/>
|
|
</WizardFormFullWidth>
|
|
<WizardFormFullWidth>
|
|
<Input
|
|
generalType="textarea"
|
|
label={texts.events.shortDescriptionLabel}
|
|
name="shortDescription"
|
|
textAreaMinRows={2}
|
|
/>
|
|
</WizardFormFullWidth>
|
|
<WizardFormFullWidth>
|
|
<p className="labelClass mb-2">{texts.common.description}</p>
|
|
<TextEditor
|
|
value={form.watch('description')}
|
|
variant="default"
|
|
onChange={(value) => {
|
|
form.setValue('description', value, { shouldDirty: true })
|
|
}}
|
|
/>
|
|
</WizardFormFullWidth>
|
|
</WizardFormGrid>
|
|
</FormSection>
|
|
|
|
<FormSection
|
|
description={texts.events.mediaSectionDescription}
|
|
title={texts.events.editMediaCardTitle}
|
|
>
|
|
<EventMediaGalleryUploader
|
|
items={media}
|
|
onChange={setMedia}
|
|
/>
|
|
</FormSection>
|
|
|
|
<FormSection
|
|
description={texts.events.wizardStep2Hint}
|
|
title={texts.events.wizardStep2Label}
|
|
>
|
|
<WizardFormGrid>
|
|
<Input
|
|
required
|
|
disabled={scheduleLocked}
|
|
generalType="eventDatePicker"
|
|
label={texts.events.startsAtDateLabel}
|
|
minDate={todayIso}
|
|
name="startDate"
|
|
/>
|
|
<Input
|
|
required
|
|
disabled={scheduleLocked}
|
|
generalType="eventTimePicker"
|
|
label={texts.events.startsAtTimeLabel}
|
|
minValue={startMinimumTime}
|
|
name="startTime"
|
|
/>
|
|
<Input
|
|
required
|
|
disabled={scheduleLocked}
|
|
generalType="eventDatePicker"
|
|
label={texts.events.endsAtDateLabel}
|
|
minDate={startDate || todayIso}
|
|
name="endDate"
|
|
/>
|
|
<Input
|
|
required
|
|
disabled={scheduleLocked}
|
|
generalType="eventTimePicker"
|
|
label={texts.events.endsAtTimeLabel}
|
|
minValue={endMinimumTime}
|
|
name="endTime"
|
|
/>
|
|
<Input
|
|
required
|
|
disabled={locationLocked}
|
|
generalType="select"
|
|
label={texts.common.city}
|
|
name="cityId"
|
|
selectKey="id"
|
|
selectOptions={cities}
|
|
selectValue="name"
|
|
/>
|
|
<WizardFormFullWidth>
|
|
<Input
|
|
required
|
|
disabled={locationLocked}
|
|
generalType="textarea"
|
|
label={texts.events.address}
|
|
name="address"
|
|
textAreaMinRows={3}
|
|
/>
|
|
</WizardFormFullWidth>
|
|
<WizardFormFullWidth>
|
|
<MapLocationPicker
|
|
disabled={locationLocked}
|
|
lat={lat}
|
|
lng={lng}
|
|
onAddressResolved={(address) => {
|
|
form.setValue('address', address, { shouldDirty: true })
|
|
}}
|
|
onChange={({ lat: nextLat, lng: nextLng }) => {
|
|
form.setValue('lat', nextLat, { shouldDirty: true })
|
|
form.setValue('lng', nextLng, { shouldDirty: true })
|
|
}}
|
|
/>
|
|
</WizardFormFullWidth>
|
|
<WizardFormFullWidth>
|
|
<Input
|
|
disabled={locationLocked}
|
|
generalType="radio"
|
|
label={texts.events.addressVisibilityLabel}
|
|
name="addressVisibility"
|
|
radioOptions={ADDRESS_VISIBILITY_OPTIONS}
|
|
selectKey="selectKey"
|
|
/>
|
|
</WizardFormFullWidth>
|
|
{addressVisibility === 'attendees_only' && (
|
|
<WizardFormFullWidth>
|
|
<Input
|
|
required
|
|
disabled={locationLocked}
|
|
generalType="textarea"
|
|
label={texts.events.generalAreaLabel}
|
|
name="generalArea"
|
|
placeholder={texts.events.generalAreaPlaceholder}
|
|
textAreaMinRows={2}
|
|
/>
|
|
</WizardFormFullWidth>
|
|
)}
|
|
</WizardFormGrid>
|
|
</FormSection>
|
|
|
|
<FormSection
|
|
description={texts.events.wizardStep3Hint}
|
|
title={texts.events.pricingCardTitle}
|
|
>
|
|
<WizardFormGrid>
|
|
<div className="flex items-end pb-1">
|
|
<Input
|
|
disabled={priceLocked}
|
|
generalType="switch"
|
|
label={texts.events.isFreeLabel}
|
|
name="isFree"
|
|
/>
|
|
</div>
|
|
<Input
|
|
disabled={priceLocked || isFree}
|
|
formatOptions={{ style: 'decimal' }}
|
|
generalType="numberInput"
|
|
label={texts.events.priceTomanLabel}
|
|
minValue={0}
|
|
name="price"
|
|
required={!isFree}
|
|
/>
|
|
<Input
|
|
required
|
|
generalType="numberInput"
|
|
label={texts.events.capacity}
|
|
minValue={policy.minCapacity}
|
|
name="capacity"
|
|
/>
|
|
<p className="text-xs text-secondary-20-span-2">
|
|
{format(texts.events.reservedCapacityEditHint, { count: event.reservedCapacity ?? 0 })}
|
|
</p>
|
|
<WizardFormFullWidth>
|
|
<Input
|
|
disabled={audienceLocked}
|
|
generalType="radio"
|
|
label={texts.events.genderRestrictionLabel}
|
|
name="genderRestriction"
|
|
radioOptions={[...GENDER_RESTRICTION_OPTIONS]}
|
|
selectKey="selectKey"
|
|
/>
|
|
</WizardFormFullWidth>
|
|
<WizardFormFullWidth>
|
|
<Input
|
|
disabled={audienceLocked}
|
|
generalType="radio"
|
|
label={texts.events.ageRestrictionLabel}
|
|
name="ageRestriction"
|
|
radioOptions={[...AGE_RESTRICTION_OPTIONS]}
|
|
selectKey="selectKey"
|
|
/>
|
|
</WizardFormFullWidth>
|
|
<Input
|
|
disabled={cancellationFeeLocked}
|
|
generalType="numberInput"
|
|
label={texts.events.cancellationFeeMoreThan24HoursLabel}
|
|
maxValue={100}
|
|
minValue={0}
|
|
name="cancellationFeePercentMoreThan24Hours"
|
|
/>
|
|
<Input
|
|
disabled={cancellationFeeLocked}
|
|
generalType="numberInput"
|
|
label={texts.events.cancellationFee12To24HoursLabel}
|
|
maxValue={100}
|
|
minValue={0}
|
|
name="cancellationFeePercent12To24Hours"
|
|
/>
|
|
<Input
|
|
disabled={cancellationFeeLocked}
|
|
generalType="numberInput"
|
|
label={texts.events.cancellationFeeLabel}
|
|
maxValue={100}
|
|
minValue={0}
|
|
name="cancellationFeePercent"
|
|
/>
|
|
<div className="flex items-end pb-1">
|
|
<Input
|
|
disabled={behavioralLocked}
|
|
generalType="switch"
|
|
label={texts.events.discoverableLabel}
|
|
name="isDiscoverable"
|
|
/>
|
|
</div>
|
|
<div className="flex items-end pb-1">
|
|
<Input
|
|
disabled={behavioralLocked}
|
|
generalType="switch"
|
|
label={texts.events.autoCreateGroupAfterEndLabel}
|
|
name="autoCreateGroup"
|
|
/>
|
|
</div>
|
|
<div className="flex items-end pb-1">
|
|
<Input
|
|
disabled={behavioralLocked}
|
|
generalType="switch"
|
|
label={texts.events.waitlistAutoOfferLabel}
|
|
name="waitlistAutoOffer"
|
|
/>
|
|
</div>
|
|
<div className="flex items-end pb-1">
|
|
<Input
|
|
disabled={behavioralLocked}
|
|
generalType="switch"
|
|
label={texts.events.sendReviewSmsAfterEndLabel}
|
|
name="sendReviewRequestSms"
|
|
/>
|
|
</div>
|
|
</WizardFormGrid>
|
|
</FormSection>
|
|
|
|
<FormSection
|
|
description={texts.events.faqSectionDescription}
|
|
title={texts.events.editFaqCardTitle}
|
|
>
|
|
<FaqEditor
|
|
items={faqs}
|
|
onChange={setFaqs}
|
|
/>
|
|
</FormSection>
|
|
|
|
<FormActions
|
|
className="sticky bottom-3 z-20"
|
|
helper={<UnsavedChangesIndicator isDirty={form.formState.isDirty} />}
|
|
>
|
|
<Button
|
|
isLoading={isSaving}
|
|
type="submit"
|
|
>
|
|
{texts.common.saveChanges}
|
|
</Button>
|
|
<Button
|
|
variant="flat"
|
|
onClick={() => {
|
|
router.push(cancelHref)
|
|
}}
|
|
>
|
|
{texts.common.cancel}
|
|
</Button>
|
|
</FormActions>
|
|
</form>
|
|
</FormProvider>
|
|
)
|
|
}
|
|
|
|
export default EventEditForm
|