diff --git a/frontend/src/features/offerings/components/OfferingForm.tsx b/frontend/src/features/offerings/components/OfferingForm.tsx index 37df333..11d8124 100644 --- a/frontend/src/features/offerings/components/OfferingForm.tsx +++ b/frontend/src/features/offerings/components/OfferingForm.tsx @@ -1,11 +1,13 @@ +import { useState } from 'react'; import { useForm, useWatch } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { useTranslation } from 'react-i18next'; -import { Plus, Trash2 } from 'lucide-react'; +import { Check, Pencil, Plus, Trash2, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { FieldError } from '@/components/ui/FieldError'; +import { slugifyPreview } from '@/lib/slugify'; import { offeringFormSchema, type OfferingFormData } from '../schemas/offering'; import type { OfferingAdminDto } from '../services/types'; @@ -23,7 +25,8 @@ export function OfferingForm({ initialValues, onSubmit, isSubmitting }: Offering control, handleSubmit, setValue, - formState: { errors }, + resetField, + formState: { errors, dirtyFields }, } = useForm({ resolver: zodResolver(offeringFormSchema), mode: 'onTouched', @@ -35,12 +38,41 @@ export function OfferingForm({ initialValues, onSubmit, isSubmitting }: Offering features: [''], ctaLabel: '', featured: false, + slug: '', }, }); // useFieldArray requires object-shaped array items — features is a plain string[], // so add/remove are handled directly via setValue instead. const features = useWatch({ control, name: 'features' }); + const title = useWatch({ control, name: 'title' }); + const slug = useWatch({ control, name: 'slug' }); + const [isEditingSlug, setIsEditingSlug] = useState(false); + + // Once the admin has manually set a non-empty slug, it stops tracking the title — mirrors + // OfferingsService: an explicit Slug always wins, an untouched one keeps auto-regenerating. + const isManuallySet = dirtyFields.slug === true && slug !== undefined && slug.length > 0; + const displaySlug = isManuallySet ? slug : slugifyPreview(title ?? ''); + + function confirmSlugEdit() { + setIsEditingSlug(false); + } + + function cancelSlugEdit() { + resetField('slug'); + setIsEditingSlug(false); + } + + // Slug is auto-generated from the title server-side unless explicitly overridden here — + // only send it along when the admin actually touched this field, so an untouched slug + // keeps auto-regenerating when the title changes (see OfferingsService.UpdateAsync). + function submitWithSlugPolicy(values: OfferingFormData) { + const payload = { ...values }; + if (!isManuallySet) { + delete payload.slug; + } + onSubmit(payload); + } function addFeature() { setValue('features', [...features, ''], { shouldValidate: true }); @@ -51,13 +83,69 @@ export function OfferingForm({ initialValues, onSubmit, isSubmitting }: Offering } return ( -
+
{errors.title && }
+
+ {isEditingSlug ? ( +
+ + + + +
+ ) : ( +
+ {t('offerings.form.slugLabel')}: + + {displaySlug.length > 0 ? displaySlug : t('offerings.form.slugAutoHint')} + + +
+ )} + {errors.slug && } +
+