diff --git a/config.json b/config.json index 68e908a..6240778 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { "id": "events", - "version": "1.6.1-aio.10", + "version": "1.6.1-aio.13", "name": "Events", "repo": "https://git.atitlan.io/aiolabs/events", "short_description": "Sell and register event tickets", diff --git a/static/js/index.js b/static/js/index.js index 646b37b..40f98a4 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -67,13 +67,15 @@ window.PageEvents = { name: 'event_start_date', align: 'left', label: 'Start date', - field: 'event_start_date' + field: 'event_start_date', + format: val => this.formatEventDate(val) }, { name: 'event_end_date', align: 'left', label: 'End date', - field: 'event_end_date' + field: 'event_end_date', + format: val => this.formatEventDate(val) }, { name: 'closing_date', @@ -149,12 +151,58 @@ window.PageEvents = { rowsPerPage: 10 } }, + // Rails an organizer can enable per event. Mirrors the webapp's + // CreateEventDialog; `fiat` is rendered disabled when the LNbits user + // has no fiat provider (see `hasFiatProvider`). + paymentMethodOptions: [ + { + value: 'lightning', + label: 'Lightning', + hint: 'Pay with any Lightning wallet' + }, + { + value: 'fiat', + label: 'Card', + hint: 'Card or bank through your configured fiat provider' + } + ], + // Same list the webapp offers (src/modules/events/types/category.ts); + // published as NIP-52 `t` tags so both clients filter on one vocabulary. + categoryOptions: [ + 'concert', + 'workshop', + 'market', + 'festival', + 'exhibition', + 'sport', + 'theater', + 'cinema', + 'party', + 'talk', + 'conference', + 'meetup', + 'food', + 'outdoor', + 'kids', + 'wellness', + 'technology', + 'art', + 'music', + 'dance', + 'literature', + 'comedy', + 'charity', + 'tradition', + 'other' + ].map(c => ({label: c.charAt(0).toUpperCase() + c.slice(1), value: c})), formDialog: { show: false, data: { currency: 'sats', allow_fiat: false, fiat_currency: 'GBP', + location: '', + categories: [], extra: { payment_methods: ['lightning'], promo_codes: [], @@ -165,6 +213,27 @@ window.PageEvents = { } } }, + computed: { + hasFiatProvider() { + return (this.g.user?.fiat_providers || []).length > 0 + }, + fiatProviderNames() { + return (this.g.user?.fiat_providers || []) + .map(p => p.charAt(0).toUpperCase() + p.slice(1)) + .join(', ') + }, + acceptsFiat() { + return (this.formDialog.data.extra?.payment_methods || []).includes( + 'fiat' + ) + }, + isSatPrice() { + return !this.isFiatCurrency(this.formDialog.data.currency) + }, + fiatCurrencyOptions() { + return this.currencies.filter(c => this.isFiatCurrency(c)) + } + }, methods: { isFiatCurrency(currency) { return !['sat', 'sats'].includes((currency || '').toLowerCase()) @@ -382,29 +451,62 @@ window.PageEvents = { }, splitDateTime(value) { // Inverse of foldDateTime: split a stored string back into the - // day/time pieces the form inputs bind to. + // day/time pieces the form inputs bind to. Slicing to HH:MM also + // drops the seconds + offset suffix withLocalTzOffset stamps on + // submit, so the organizer sees the wall-clock they entered. if (!value) return {day: '', time: ''} const [day, time = ''] = value.split('T') // Time inputs only accept HH:MM, drop any seconds we stored. return {day, time: time.slice(0, 5)} }, + withLocalTzOffset(value) { + // Stamp the browser's UTC offset on a "YYYY-MM-DDTHH:MM" value. + // The publisher's `_to_unix` treats a naive datetime as UTC, so an + // event entered as 18:00 in CEST would otherwise go out on Nostr + // as 18:00 UTC. Same transform the webapp applies; date-only + // values pass through unchanged (they map to NIP-52 kind 31922). + if (!value || !value.includes('T')) return value + const offMin = -new Date(value).getTimezoneOffset() + const sign = offMin >= 0 ? '+' : '-' + const abs = Math.abs(offMin) + const hh = String(Math.floor(abs / 60)).padStart(2, '0') + const mm = String(abs % 60).padStart(2, '0') + return `${value}:00${sign}${hh}:${mm}` + }, + formatEventDate(value) { + // Table display: "YYYY-MM-DD" or "YYYY-MM-DD HH:MM". + if (!value) return '' + const {day, time} = this.splitDateTime(value) + return time ? `${day} ${time}` : day + }, + validateEndDate() { + // Cross-field rule for the end-day input: end >= start, compared + // on the folded date+time so an equal-day earlier time is caught. + const d = this.formDialog.data + const start = this.foldDateTime(d.event_start_day, d.event_start_time) + const end = this.foldDateTime(d.event_end_day, d.event_end_time) + if (!start || !end) return true + return end >= start || 'End must be on or after start' + }, sendEventData() { const wallet = _.findWhere(this.g.user.wallets, { id: this.formDialog.data.wallet }) const data = {...this.formDialog.data} - data.event_start_date = this.foldDateTime( - data.event_start_day, - data.event_start_time + data.event_start_date = this.withLocalTzOffset( + this.foldDateTime(data.event_start_day, data.event_start_time) ) - data.event_end_date = this.foldDateTime( - data.event_end_day, - data.event_end_time + data.event_end_date = this.withLocalTzOffset( + this.foldDateTime(data.event_end_day, data.event_end_time) ) delete data.event_start_day delete data.event_start_time delete data.event_end_day delete data.event_end_time + // Optional NIP-52 fields: blank location is "unset", not "". + data.location = (data.location || '').trim() || null + data.categories = data.categories || [] + data.closing_date = data.closing_date || null if (data.extra?.promo_codes) { data.extra.promo_codes = data.extra.promo_codes @@ -425,10 +527,12 @@ window.PageEvents = { // allow_fiat stays the fiat-currency carrier the backend and the // NIP-52 tags read; keep it in lockstep with the checkbox list. data.allow_fiat = methods.includes('fiat') - if (!this.isFiatCurrency(data.currency)) { - if (!data.allow_fiat) { - data.fiat_currency = 'GBP' - } + if (this.isFiatCurrency(data.currency)) { + // A fiat-priced event settles in its price currency; mirror it so + // the payload (and the tickets_fiat_currency tag) stay coherent. + data.fiat_currency = data.currency + } else if (!data.allow_fiat) { + data.fiat_currency = 'GBP' } if (data.id) { @@ -453,6 +557,8 @@ window.PageEvents = { ? data.extra.payment_methods : ['lightning', ...(data.allow_fiat ? ['fiat'] : [])] }, + location: data.location || '', + categories: [...(data.categories || [])], event_start_day: start.day, event_start_time: start.time, event_end_day: end.day, @@ -463,6 +569,8 @@ window.PageEvents = { currency: 'sats', allow_fiat: false, fiat_currency: 'GBP', + location: '', + categories: [], event_start_day: '', event_start_time: '', event_end_day: '', @@ -487,6 +595,8 @@ window.PageEvents = { currency: 'sats', allow_fiat: false, fiat_currency: 'GBP', + location: '', + categories: [], extra: { payment_methods: ['lightning'], email_notifications: false, diff --git a/static/js/index.vue b/static/js/index.vue index 502d9ad..6e7ec28 100644 --- a/static/js/index.vue +++ b/static/js/index.vue @@ -479,8 +479,10 @@ filled dense v-model.trim="formDialog.data.name" - type="name" - label="Title of event " + type="text" + label="Title of event *" + lazy-rules + :rules="[val => !!val || 'Title is required']" >
@@ -504,6 +506,30 @@ label="Info about the event" hint="Markdown supported" > + + + + -
-
Ticket closing date
-
- -
-
-
Event begins
+
Event begins *
@@ -552,6 +569,10 @@ dense v-model.trim="formDialog.data.event_end_day" type="date" + hint="Defaults to the start date" + lazy-rules + reactive-rules + :rules="[validateEndDate]" >
@@ -564,6 +585,18 @@ >
+
+
Ticket sales close
+
+ +
+
@@ -581,7 +614,9 @@ dense v-model.number="formDialog.data.amount_tickets" type="number" - label="Amount of tickets " + min="0" + label="Amount of tickets" + hint="0 = unlimited" >
@@ -600,34 +635,57 @@
-
Payment methods *
- +
Payment methods *
- Card / fiat checkout goes through the fiat provider configured on - this LNbits instance. Untick Lightning for a card-only event. + Pick the rails buyers can pay with. Untick Lightning for a + card-only sale. +
+
+
+ + + +
+ + +
+ + Your LNbits user has no fiat provider configured. Ask the + instance admin to enable Stripe, PayPal or Square (Admin → + Fiat providers) to accept card payments. + +
+
Create Event