diff --git a/src/modules/activities/components/CreateEventDialog.vue b/src/modules/activities/components/CreateEventDialog.vue index 2a9861a..1855049 100644 --- a/src/modules/activities/components/CreateEventDialog.vue +++ b/src/modules/activities/components/CreateEventDialog.vue @@ -24,7 +24,6 @@ import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' -import { Separator } from '@/components/ui/separator' import { ScrollArea } from '@/components/ui/scroll-area' import { Select, @@ -33,15 +32,12 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from '@/components/ui/collapsible' -import { Calendar, Loader2, ChevronDown, MapPin } from 'lucide-vue-next' +import { Calendar, Loader2, MapPin } from 'lucide-vue-next' import { toastService } from '@/core/services/ToastService' import { injectService, SERVICE_TOKENS } from '@/core/di-container' import ImageUpload from '@/modules/base/components/ImageUpload.vue' +import DatePicker from '@/modules/base/components/DatePicker.vue' +import TimePicker from '@/modules/base/components/TimePicker.vue' import type { ImageUploadService, UploadedImage } from '@/modules/base/services/ImageUploadService' import type { TicketApiService } from '../services/TicketApiService' import type { CreateEventRequest } from '../types/ticket' @@ -60,18 +56,45 @@ const emit = defineEmits<{ const { t } = useI18n() -const formSchema = toTypedSchema(z.object({ - name: z.string().min(1, "Title is required").max(200, "Title too long"), - info: z.string().max(2000, "Description too long").optional().default(''), - event_start_date: z.string().min(1, "Start date is required"), - event_start_time: z.string().optional().default(''), - event_end_date: z.string().optional().default(''), - event_end_time: z.string().optional().default(''), - location: z.string().max(500).optional().default(''), - currency: z.string().default("sat"), - amount_tickets: z.number().min(0).max(100000).default(0), - price_per_ticket: z.number().min(0).default(0), -})) +// Fold a date input ("YYYY-MM-DD") and an optional time input ("HH:MM") +// into the events-extension wire format: date-only when no time given, +// ISO 8601 datetime otherwise. The publisher switches NIP-52 kinds on +// the "T" delimiter. Hoisted above the schema so the validation refine +// can reuse it. +function foldDateTime(date: string, time: string): string { + if (!date) return '' + return time ? `${date}T${time}` : date +} + +const formSchema = toTypedSchema( + z + .object({ + name: z.string().min(1, "Title is required").max(200, "Title too long"), + info: z.string().max(2000, "Description too long").optional().default(''), + event_start_date: z.string().min(1, "Start date is required"), + event_start_time: z.string().optional().default(''), + event_end_date: z.string().optional().default(''), + event_end_time: z.string().optional().default(''), + location: z.string().max(500).optional().default(''), + currency: z.string().default("sat"), + amount_tickets: z.number().min(0).max(100000).default(0), + price_per_ticket: z.number().min(0).default(0), + }) + .superRefine((v, ctx) => { + // End must not precede start. Compare on the folded date+time + // string so equal-date / later-time is enforced too. + if (!v.event_end_date) return + const start = foldDateTime(v.event_start_date, v.event_start_time) + const end = foldDateTime(v.event_end_date, v.event_end_time) + if (start && end && end < start) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['event_end_date'], + message: 'End must be on or after start', + }) + } + }) +) const form = useForm({ validationSchema: formSchema, @@ -94,14 +117,21 @@ interface BannerImage extends UploadedImage { } const bannerImages = ref([]) -// Fold a date input ("YYYY-MM-DD") and an optional time input ("HH:MM") -// into the events-extension wire format: date-only when no time given, -// ISO 8601 datetime otherwise. The publisher switches NIP-52 kinds on -// the "T" delimiter. -function foldDateTime(date: string, time: string): string { - if (!date) return '' - return time ? `${date}T${time}` : date -} +// Auto-mirror end date to start: when the user picks a start date, +// surface that same date in the end-date picker so a one-day event +// requires no extra clicks. Don't overwrite an end date the user +// already set *after* the start — only fill when empty or when the +// existing end has fallen behind the new start. +watch( + () => form.values.event_start_date, + (start, prev) => { + if (!start) return + const end = form.values.event_end_date + if (!end || end < start || end === prev) { + form.setFieldValue('event_end_date', start) + } + } +) const paymentService = injectService(SERVICE_TOKENS.PAYMENT_SERVICE) const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService | null @@ -110,7 +140,6 @@ const imageService = injectService(SERVICE_TOKENS.IMAGE_UPLO const availableCurrencies = ref(['sat']) const loadingCurrencies = ref(false) const selectedCategories = ref([]) -const showMoreOptions = ref(false) watch(() => props.open, async (isOpen) => { if (isOpen && ticketApi && !loadingCurrencies.value) { @@ -125,7 +154,6 @@ watch(() => props.open, async (isOpen) => { } if (!isOpen) { selectedCategories.value = [] - showMoreOptions.value = false } }) @@ -240,21 +268,67 @@ const handleOpenChange = (open: boolean) => {
- + Start date * - + - + Start time - + + + + + +
+ + +
+ + + End date + + + + + + + + + + + End time + (optional) + + + @@ -371,42 +445,6 @@ const handleOpenChange = (open: boolean) => {
- - - - - - - - -
- - - End date - - - - Defaults to start date - - - - - - - End time - - - - - - -
-
-
-