Compare commits

...

1 commit

Author SHA1 Message Date
67583b7074 feat(events): organizer name + reply-to fields in the event editor
Pairs with aiolabs/events v1.6.1-aio.11: `extra.organizer_name` becomes
the From display name of ticket emails ("Organizer via <site>") and
`extra.reply_to_email` the Reply-To (blank = the organizer's account
email). Both live under the notifications section next to the per-event
subject/body; the reply-to is validated as an email or left empty.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
2026-09-09 12:34:26 +02:00
2 changed files with 51 additions and 0 deletions

View file

@ -71,6 +71,10 @@ interface Props {
autoApprove?: boolean autoApprove?: boolean
} }
// Instance name used in the From display name of ticket emails
// (the LNbits site title on the backend; the standalone's brand here).
const siteName = (import.meta.env.VITE_APP_NAME as string) || 'Events'
const props = defineProps<Props>() const props = defineProps<Props>()
const emit = defineEmits<{ const emit = defineEmits<{
'update:open': [value: boolean] 'update:open': [value: boolean]
@ -144,6 +148,12 @@ const formSchema = toTypedSchema(
nostr_notifications: z.boolean().default(false), nostr_notifications: z.boolean().default(false),
notification_subject: z.string().max(200).default(''), notification_subject: z.string().max(200).default(''),
notification_body: z.string().max(2000).default(''), notification_body: z.string().max(2000).default(''),
organizer_name: z.string().trim().max(100).default(''),
reply_to_email: z
.string()
.trim()
.default('')
.refine((v) => v === '' || z.string().email().safeParse(v).success, 'Invalid email address'),
}) })
.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
@ -193,6 +203,8 @@ const form = useForm({
nostr_notifications: false, nostr_notifications: false,
notification_subject: '', notification_subject: '',
notification_body: '', notification_body: '',
organizer_name: '',
reply_to_email: '',
} }
}) })
@ -299,6 +311,8 @@ async function populateFromEvent(event: TicketedEvent) {
nostr_notifications: event.extra?.nostr_notifications ?? false, nostr_notifications: event.extra?.nostr_notifications ?? false,
notification_subject: event.extra?.notification_subject ?? '', notification_subject: event.extra?.notification_subject ?? '',
notification_body: event.extra?.notification_body ?? '', notification_body: event.extra?.notification_body ?? '',
organizer_name: event.extra?.organizer_name ?? '',
reply_to_email: event.extra?.reply_to_email ?? '',
}) })
selectedCategories.value = [...(event.categories ?? [])] selectedCategories.value = [...(event.categories ?? [])]
if (event.banner) { if (event.banner) {
@ -425,6 +439,8 @@ const onSubmit = form.handleSubmit(async (formValues) => {
nostr_notifications: formValues.nostr_notifications, nostr_notifications: formValues.nostr_notifications,
notification_subject: formValues.notification_subject, notification_subject: formValues.notification_subject,
notification_body: formValues.notification_body, notification_body: formValues.notification_body,
organizer_name: formValues.organizer_name,
reply_to_email: formValues.reply_to_email || null,
} }
if (isEditMode.value) { if (isEditMode.value) {
@ -851,6 +867,35 @@ const handleOpenChange = (open: boolean) => {
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
</FormField> </FormField>
<!-- Sender identity: the From address stays the instance
mailbox (that is what DKIM signs); the organizer only
controls the display name and where replies land. -->
<FormField v-slot="{ componentField }" name="organizer_name">
<FormItem>
<FormLabel class="text-sm">Organizer name</FormLabel>
<FormControl>
<Input placeholder="Château du Faune" v-bind="componentField" />
</FormControl>
<FormDescription class="text-xs">
Ticket emails arrive from "Organizer name via {{ siteName }}".
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="reply_to_email">
<FormItem>
<FormLabel class="text-sm">Reply-to email</FormLabel>
<FormControl>
<Input type="email" inputmode="email" autocapitalize="none" placeholder="you@example.com" v-bind="componentField" />
</FormControl>
<FormDescription class="text-xs">
Where attendee replies go. Leave blank to use your account email.
</FormDescription>
<FormMessage />
</FormItem>
</FormField>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>

View file

@ -26,6 +26,12 @@ export interface EventExtra {
nostr_notifications: boolean nostr_notifications: boolean
notification_subject: string notification_subject: string
notification_body: string notification_body: string
/** Shown as "<organizer_name> via <site>" in the From line of ticket
* emails. Optional. */
organizer_name?: string
/** Reply-To for ticket emails; null/absent = the organizer's LNbits
* account email. */
reply_to_email?: string | null
/** Rails the organizer enabled (`lightning`, `fiat`). Empty / absent /** Rails the organizer enabled (`lightning`, `fiat`). Empty / absent
* means the legacy rule: Lightning always, fiat iff `allow_fiat`. * means the legacy rule: Lightning always, fiat iff `allow_fiat`.
* Same shape as upstream v2's `extra.payment_methods`. */ * Same shape as upstream v2's `extra.payment_methods`. */