feat(activities): notification config on event create + edit
CreateEventDialog gains a collapsible "Buyer notifications" section exposing the EventExtra fields added upstream in v1.4.0 / v1.6.0: - email_notifications + nostr_notifications switches — opt buyers into email and NIP-04 Nostr DM ticket confirmations. - notification_subject + notification_body inputs — let organizers customize the message. Empty falls back to extension defaults. Submit handler builds `extra` by overlaying onto the existing event.extra so unrelated fields the LNbits admin UI sets (promo_codes, conditional, min_tickets) survive the round-trip through the webapp. Populate-from-event mirrors the same. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
620919da58
commit
d2870b41b2
1 changed files with 90 additions and 0 deletions
|
|
@ -24,6 +24,9 @@ import { Input } from '@/components/ui/input'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
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 { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
|
|
@ -122,6 +125,10 @@ const formSchema = toTypedSchema(
|
||||||
currency: z.string().default("sat"),
|
currency: z.string().default("sat"),
|
||||||
amount_tickets: z.number().min(0).max(100000).default(0),
|
amount_tickets: z.number().min(0).max(100000).default(0),
|
||||||
price_per_ticket: z.number().min(0).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) => {
|
.superRefine((v, ctx) => {
|
||||||
// End must not precede start. Compare on the folded date+time
|
// End must not precede start. Compare on the folded date+time
|
||||||
|
|
@ -152,6 +159,10 @@ const form = useForm({
|
||||||
currency: 'sat',
|
currency: 'sat',
|
||||||
amount_tickets: 0,
|
amount_tickets: 0,
|
||||||
price_per_ticket: 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
|
// When `true`, suppress the auto-mirror watcher so we don't clobber an
|
||||||
// edit-mode population with start-date side effects mid-setValues.
|
// edit-mode population with start-date side effects mid-setValues.
|
||||||
const isPopulating = ref(false)
|
const isPopulating = ref(false)
|
||||||
|
const notificationsOpen = ref(false)
|
||||||
|
|
||||||
// Auto-mirror end date to start: when the user picks a start 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
|
// 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',
|
currency: event.currency ?? 'sat',
|
||||||
amount_tickets: event.amount_tickets ?? 0,
|
amount_tickets: event.amount_tickets ?? 0,
|
||||||
price_per_ticket: event.price_per_ticket ?? 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 ?? [])]
|
selectedCategories.value = [...(event.categories ?? [])]
|
||||||
if (event.banner) {
|
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 (formValues.price_per_ticket) eventData.price_per_ticket = formValues.price_per_ticket
|
||||||
if (selectedCategories.value.length > 0) eventData.categories = selectedCategories.value
|
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 (isEditMode.value) {
|
||||||
if (!props.onUpdateEvent || !props.event?.id) {
|
if (!props.onUpdateEvent || !props.event?.id) {
|
||||||
toastService.error('Update handler missing')
|
toastService.error('Update handler missing')
|
||||||
|
|
@ -591,6 +619,68 @@ const handleOpenChange = (open: boolean) => {
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</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 -->
|
<!-- Actions -->
|
||||||
<div class="flex justify-end gap-3 pt-2">
|
<div class="flex justify-end gap-3 pt-2">
|
||||||
<Button type="button" variant="outline" @click="handleOpenChange(false)" :disabled="isLoading">
|
<Button type="button" variant="outline" @click="handleOpenChange(false)" :disabled="isLoading">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue