From 67583b7074163a8b9448107551f664b58280a3ab Mon Sep 17 00:00:00 2001 From: Padreug Date: Wed, 9 Sep 2026 12:34:26 +0200 Subject: [PATCH 1/8] 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 ") 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 Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo --- .../events/components/CreateEventDialog.vue | 45 +++++++++++++++++++ src/modules/events/types/ticket.ts | 6 +++ 2 files changed, 51 insertions(+) diff --git a/src/modules/events/components/CreateEventDialog.vue b/src/modules/events/components/CreateEventDialog.vue index ee8f487..bb7f20e 100644 --- a/src/modules/events/components/CreateEventDialog.vue +++ b/src/modules/events/components/CreateEventDialog.vue @@ -71,6 +71,10 @@ interface Props { 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() const emit = defineEmits<{ 'update:open': [value: boolean] @@ -144,6 +148,12 @@ const formSchema = toTypedSchema( nostr_notifications: z.boolean().default(false), notification_subject: z.string().max(200).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) => { // End must not precede start. Compare on the folded date+time @@ -193,6 +203,8 @@ const form = useForm({ nostr_notifications: false, notification_subject: '', notification_body: '', + organizer_name: '', + reply_to_email: '', } }) @@ -299,6 +311,8 @@ async function populateFromEvent(event: TicketedEvent) { nostr_notifications: event.extra?.nostr_notifications ?? false, notification_subject: event.extra?.notification_subject ?? '', notification_body: event.extra?.notification_body ?? '', + organizer_name: event.extra?.organizer_name ?? '', + reply_to_email: event.extra?.reply_to_email ?? '', }) selectedCategories.value = [...(event.categories ?? [])] if (event.banner) { @@ -425,6 +439,8 @@ const onSubmit = form.handleSubmit(async (formValues) => { nostr_notifications: formValues.nostr_notifications, notification_subject: formValues.notification_subject, notification_body: formValues.notification_body, + organizer_name: formValues.organizer_name, + reply_to_email: formValues.reply_to_email || null, } if (isEditMode.value) { @@ -851,6 +867,35 @@ const handleOpenChange = (open: boolean) => { + + + + + Organizer name + + + + + Ticket emails arrive from "Organizer name via {{ siteName }}". + + + + + + + + Reply-to email + + + + + Where attendee replies go. Leave blank to use your account email. + + + + diff --git a/src/modules/events/types/ticket.ts b/src/modules/events/types/ticket.ts index d42b3c2c..533db2f 100644 --- a/src/modules/events/types/ticket.ts +++ b/src/modules/events/types/ticket.ts @@ -26,6 +26,12 @@ export interface EventExtra { nostr_notifications: boolean notification_subject: string notification_body: string + /** Shown as " via " 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 * means the legacy rule: Lightning always, fiat iff `allow_fiat`. * Same shape as upstream v2's `extra.payment_methods`. */ From 9eb75a201754dfe462b891507e144f839f9d63d9 Mon Sep 17 00:00:00 2001 From: Padreug Date: Wed, 9 Sep 2026 12:47:38 +0200 Subject: [PATCH 2/8] feat(events): surface the buy-ticket CTA above the description + sticky bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ticket block (owned banner, Buy ticket + availability, past/sold-out notices, and the host's Scan CTA) moved from below the description to directly under the date and location. A long description was pushing the primary action below the fold — on the Château du Faune event the button sat under ~1.7k characters of text. A sticky bar (title, price, tickets left, the same CTA) pins above the bottom nav whenever the inline block is scrolled out of view (useElementVisibility), and hides again when it is back, or while the purchase / return dialogs are open. Container gets extra bottom padding while the bar is up so the organizer card is never covered. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01ByAwHU4pRnyE58YocQvAas --- src/modules/events/views/EventDetailPage.vue | 117 ++++++++++++++++--- 1 file changed, 99 insertions(+), 18 deletions(-) diff --git a/src/modules/events/views/EventDetailPage.vue b/src/modules/events/views/EventDetailPage.vue index ee08ae8..d4d98dd 100644 --- a/src/modules/events/views/EventDetailPage.vue +++ b/src/modules/events/views/EventDetailPage.vue @@ -1,5 +1,6 @@