feat(activities): notification config on event create + edit #69

Merged
padreug merged 1 commit from notification-config into dev 2026-05-23 21:19:06 +00:00

View file

@ -24,6 +24,9 @@ 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 { Switch } from '@/components/ui/switch'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
import { Bell, ChevronDown } from 'lucide-vue-next'
import { ScrollArea } from '@/components/ui/scroll-area'
import {
Select,
@ -122,6 +125,10 @@ const formSchema = toTypedSchema(
currency: z.string().default("sat"),
amount_tickets: z.number().min(0).max(100000).default(0),
price_per_ticket: z.number().min(0).default(0),
email_notifications: z.boolean().default(false),
nostr_notifications: z.boolean().default(false),
notification_subject: z.string().max(200).default(''),
notification_body: z.string().max(2000).default(''),
})
.superRefine((v, ctx) => {
// End must not precede start. Compare on the folded date+time
@ -152,6 +159,10 @@ const form = useForm({
currency: 'sat',
amount_tickets: 0,
price_per_ticket: 0,
email_notifications: false,
nostr_notifications: false,
notification_subject: '',
notification_body: '',
}
})
@ -174,6 +185,7 @@ function splitDateTime(value: string | null | undefined): { date: string; time:
// When `true`, suppress the auto-mirror watcher so we don't clobber an
// edit-mode population with start-date side effects mid-setValues.
const isPopulating = ref(false)
const notificationsOpen = ref(false)
// 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
@ -215,6 +227,10 @@ async function populateFromEvent(event: TicketedEvent) {
currency: event.currency ?? 'sat',
amount_tickets: event.amount_tickets ?? 0,
price_per_ticket: event.price_per_ticket ?? 0,
email_notifications: event.extra?.email_notifications ?? false,
nostr_notifications: event.extra?.nostr_notifications ?? false,
notification_subject: event.extra?.notification_subject ?? '',
notification_body: event.extra?.notification_body ?? '',
})
selectedCategories.value = [...(event.categories ?? [])]
if (event.banner) {
@ -322,6 +338,18 @@ const onSubmit = form.handleSubmit(async (formValues) => {
if (formValues.price_per_ticket) eventData.price_per_ticket = formValues.price_per_ticket
if (selectedCategories.value.length > 0) eventData.categories = selectedCategories.value
// Notification config goes inside the `extra` envelope. On edit
// overlay onto the existing event.extra so unrelated fields the
// LNbits admin UI sets (promo_codes, conditional, min_tickets)
// survive the round-trip.
eventData.extra = {
...(props.event?.extra ?? {}),
email_notifications: formValues.email_notifications,
nostr_notifications: formValues.nostr_notifications,
notification_subject: formValues.notification_subject,
notification_body: formValues.notification_body,
}
if (isEditMode.value) {
if (!props.onUpdateEvent || !props.event?.id) {
toastService.error('Update handler missing')
@ -591,6 +619,68 @@ const handleOpenChange = (open: boolean) => {
</FormField>
</div>
<!-- Ticket buyer notifications (collapsible). The backend
sends email + NIP-04 Nostr DM confirmations on
payment when these are on. notification_subject /
body let the organizer customize the message; empty
strings fall back to the extension's defaults. -->
<Collapsible v-model:open="notificationsOpen">
<CollapsibleTrigger as-child>
<Button type="button" variant="ghost" size="sm" class="w-full justify-between text-muted-foreground">
<span class="flex items-center gap-1.5">
<Bell class="w-4 h-4" />
Buyer notifications
</span>
<ChevronDown class="w-4 h-4 transition-transform" :class="{ 'rotate-180': notificationsOpen }" />
</Button>
</CollapsibleTrigger>
<CollapsibleContent class="space-y-3 pt-2">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<FormField v-slot="{ value, handleChange }" name="email_notifications">
<FormItem class="flex flex-row items-center justify-between rounded-md border p-3">
<FormLabel class="text-sm">Email confirmation</FormLabel>
<FormControl>
<Switch :model-value="value as boolean" @update:model-value="handleChange" />
</FormControl>
</FormItem>
</FormField>
<FormField v-slot="{ value, handleChange }" name="nostr_notifications">
<FormItem class="flex flex-row items-center justify-between rounded-md border p-3">
<FormLabel class="text-sm">Nostr DM confirmation</FormLabel>
<FormControl>
<Switch :model-value="value as boolean" @update:model-value="handleChange" />
</FormControl>
</FormItem>
</FormField>
</div>
<FormField v-slot="{ componentField }" name="notification_subject">
<FormItem>
<FormLabel class="text-sm">Subject</FormLabel>
<FormControl>
<Input placeholder="Your ticket for {event_name}" v-bind="componentField" />
</FormControl>
<FormDescription class="text-xs">Leave blank to use the default.</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="notification_body">
<FormItem>
<FormLabel class="text-sm">Body</FormLabel>
<FormControl>
<Textarea placeholder="See you there!" rows="3" v-bind="componentField" />
</FormControl>
<FormDescription class="text-xs">
Leave blank to use the default. The ticket link is appended automatically.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
</CollapsibleContent>
</Collapsible>
<!-- Actions -->
<div class="flex justify-end gap-3 pt-2">
<Button type="button" variant="outline" @click="handleOpenChange(false)" :disabled="isLoading">