feat: make events dynamic (#43)
--------- Co-authored-by: dni <office@dnilabs.com>
This commit is contained in:
parent
f06bd9a668
commit
9e477ac959
21 changed files with 1164 additions and 1143 deletions
|
|
@ -1,8 +1,9 @@
|
|||
window.app = Vue.createApp({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
window.PageEventsDisplay = {
|
||||
template: '#page-events-display',
|
||||
data() {
|
||||
return {
|
||||
eventErrorLabel: '',
|
||||
event: null,
|
||||
paymentReq: null,
|
||||
redirectUrl: null,
|
||||
formDialog: {
|
||||
|
|
@ -23,15 +24,14 @@ window.app = Vue.createApp({
|
|||
show: false,
|
||||
status: 'pending',
|
||||
paymentReq: null
|
||||
}
|
||||
},
|
||||
paymentDismissMsg: null,
|
||||
paymentWebsocket: null
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.info = event_info
|
||||
this.info = this.info.substring(1, this.info.length - 1)
|
||||
this.banner = event_banner
|
||||
this.extra = event_extra
|
||||
this.hasPromoCodes = has_promoCodes
|
||||
this.eventId = this.$route.params.id
|
||||
this.event = await this.getEvent()
|
||||
},
|
||||
computed: {
|
||||
formatDescription() {
|
||||
|
|
@ -39,6 +39,18 @@ window.app = Vue.createApp({
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
async getEvent() {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/events/api/v1/events/${this.eventId}`
|
||||
)
|
||||
return data
|
||||
} catch (error) {
|
||||
this.eventErrorLabel = 'Event unavailable.'
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
resetForm(e) {
|
||||
e.preventDefault()
|
||||
this.formDialog.data.name = ''
|
||||
|
|
@ -47,10 +59,14 @@ window.app = Vue.createApp({
|
|||
},
|
||||
|
||||
closeReceiveDialog() {
|
||||
const checker = this.receive.paymentChecker
|
||||
dismissMsg()
|
||||
clearInterval(paymentChecker)
|
||||
setTimeout(() => {}, 10000)
|
||||
if (this.paymentDismissMsg) {
|
||||
this.paymentDismissMsg()
|
||||
this.paymentDismissMsg = null
|
||||
}
|
||||
if (this.paymentWebsocket) {
|
||||
this.paymentWebsocket.close()
|
||||
this.paymentWebsocket = null
|
||||
}
|
||||
},
|
||||
nameValidation(val) {
|
||||
const regex = /[`!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~]/g
|
||||
|
|
@ -63,68 +79,93 @@ window.app = Vue.createApp({
|
|||
const regex = /^[\w\.-]+@[a-zA-Z\d\.-]+\.[a-zA-Z]{2,}$/
|
||||
return regex.test(val) || 'Please enter valid email.'
|
||||
},
|
||||
Invoice() {
|
||||
axios
|
||||
.post(`/events/api/v1/tickets/${event_id}`, {
|
||||
name: this.formDialog.data.name,
|
||||
email: this.formDialog.data.email,
|
||||
promo_code: this.formDialog.data.promo_code || null
|
||||
})
|
||||
.then(response => {
|
||||
this.paymentReq = response.data.payment_request
|
||||
this.paymentCheck = response.data.payment_hash
|
||||
|
||||
dismissMsg = Quasar.Notify.create({
|
||||
timeout: 0,
|
||||
message: 'Waiting for payment...'
|
||||
})
|
||||
|
||||
this.receive = {
|
||||
show: true,
|
||||
status: 'pending',
|
||||
paymentReq: this.paymentReq
|
||||
paymentSuccess(paymentHash) {
|
||||
if (this.paymentDismissMsg) {
|
||||
this.paymentDismissMsg()
|
||||
this.paymentDismissMsg = null
|
||||
}
|
||||
this.paymentReq = null
|
||||
this.formDialog.data.name = ''
|
||||
this.formDialog.data.email = ''
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Sent, thank you!',
|
||||
icon: null
|
||||
})
|
||||
this.receive = {
|
||||
show: false,
|
||||
status: 'complete',
|
||||
paymentReq: null
|
||||
}
|
||||
this.ticketLink = {
|
||||
show: true,
|
||||
data: {
|
||||
link: `/events/ticket/${paymentHash}`
|
||||
}
|
||||
}
|
||||
setTimeout(() => {
|
||||
window.location.href = `/events/ticket/${paymentHash}`
|
||||
}, 5000)
|
||||
},
|
||||
async createInvoice() {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
`/events/api/v1/tickets/${this.eventId}`,
|
||||
null,
|
||||
{
|
||||
name: this.formDialog.data.name,
|
||||
email: this.formDialog.data.email,
|
||||
promo_code: this.formDialog.data.promo_code || null,
|
||||
refund_address: this.formDialog.data.refund || null
|
||||
}
|
||||
paymentChecker = setInterval(() => {
|
||||
axios
|
||||
.post(`/events/api/v1/tickets/${event_id}/${this.paymentCheck}`, {
|
||||
event: event_id,
|
||||
event_name: event_name,
|
||||
name: this.formDialog.data.name,
|
||||
email: this.formDialog.data.email
|
||||
})
|
||||
.then(res => {
|
||||
if (res.data.paid) {
|
||||
clearInterval(paymentChecker)
|
||||
dismissMsg()
|
||||
this.formDialog.data.name = ''
|
||||
this.formDialog.data.email = ''
|
||||
)
|
||||
this.paymentReq = data.payment_request
|
||||
this.paymentHash = data.payment_hash
|
||||
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Sent, thank you!',
|
||||
icon: null
|
||||
})
|
||||
this.receive = {
|
||||
show: false,
|
||||
status: 'complete',
|
||||
paymentReq: null
|
||||
}
|
||||
|
||||
this.ticketLink = {
|
||||
show: true,
|
||||
data: {
|
||||
link: `/events/ticket/${res.data.ticket_id}`
|
||||
}
|
||||
}
|
||||
setTimeout(() => {
|
||||
window.location.href = `/events/ticket/${res.data.ticket_id}`
|
||||
}, 5000)
|
||||
}
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
}, 2000)
|
||||
this.paymentDismissMsg = Quasar.Notify.create({
|
||||
timeout: 0,
|
||||
message: 'Waiting for payment...'
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
this.receive = {
|
||||
show: true,
|
||||
status: 'pending',
|
||||
paymentReq: this.paymentReq
|
||||
}
|
||||
this.websocketListener(this.paymentHash)
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
websocketListener(paymentHash) {
|
||||
if (this.paymentWebsocket) {
|
||||
this.paymentWebsocket.close()
|
||||
}
|
||||
|
||||
const url = new URL(window.location)
|
||||
url.protocol = url.protocol === 'https:' ? 'wss' : 'ws'
|
||||
url.pathname = `/events/api/v1/tickets/ws/${paymentHash}`
|
||||
url.search = ''
|
||||
url.hash = ''
|
||||
|
||||
const ws = new WebSocket(url)
|
||||
this.paymentWebsocket = ws
|
||||
|
||||
ws.onmessage = event => {
|
||||
const data = JSON.parse(event.data)
|
||||
if (data.paid) {
|
||||
this.paymentSuccess(paymentHash)
|
||||
ws.close()
|
||||
}
|
||||
}
|
||||
ws.onerror = error => {
|
||||
console.error('WebSocket error:', error)
|
||||
}
|
||||
ws.onclose = () => {
|
||||
if (this.paymentWebsocket === ws) {
|
||||
this.paymentWebsocket = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
125
static/js/display.vue
Normal file
125
static/js/display.vue
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
<template id="page-events-display">
|
||||
<div v-if="event" class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-img
|
||||
v-if="event.banner"
|
||||
:src="event.banner"
|
||||
transition="slide-up"
|
||||
></q-img>
|
||||
<q-card-section class="q-pa-none">
|
||||
<h3 class="q-my-none q-pa-lg" v-text="event.name"></h3>
|
||||
<div v-html="event.info" class="q-pa-lg"></div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h5 class="q-mt-none">Buy Ticket</h5>
|
||||
<q-form @submit="createInvoice()" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.name"
|
||||
label="Your name "
|
||||
:rules="[val => nameValidation(val)]"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.email"
|
||||
type="email"
|
||||
label="Your email "
|
||||
:rules="[
|
||||
val => !!val || '* Required',
|
||||
val => emailValidation(val)
|
||||
]"
|
||||
lazy-rules
|
||||
></q-input>
|
||||
<q-input
|
||||
v-if="this.extra?.conditional"
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.refund"
|
||||
label="Refund lnadress or LNURL "
|
||||
:rules="[val => !!val || '* Required']"
|
||||
lazy-rules
|
||||
:hint="`If minimum tickets (${this.extra?.min_tickets}) are not met, refund will be sent.`"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.promo_code"
|
||||
label="(optional) Promo Code "
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="
|
||||
formDialog.data.name == '' ||
|
||||
formDialog.data.email == '' ||
|
||||
Boolean(paymentReq)
|
||||
"
|
||||
type="submit"
|
||||
>Submit</q-btn
|
||||
>
|
||||
<q-btn @click="resetForm" flat color="grey" class="q-ml-auto"
|
||||
>Clear</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card v-show="ticketLink.show" class="q-pa-lg">
|
||||
<div class="text-center q-mb-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
size="xl"
|
||||
:href="ticketLink.data.link"
|
||||
target="_blank"
|
||||
color="primary"
|
||||
type="a"
|
||||
>Link to your ticket!</q-btn
|
||||
>
|
||||
<br /><br />
|
||||
<p>You'll be redirected in a few moments...</p>
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="receive.show" position="top" @hide="closeReceiveDialog">
|
||||
<q-card
|
||||
v-if="!receive.paymentReq"
|
||||
class="q-pa-lg q-pt-xl lnbits__dialog-card"
|
||||
>
|
||||
</q-card>
|
||||
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<div class="text-center q-mb-lg">
|
||||
<lnbits-qrcode
|
||||
:href="'lightning:' + receive.paymentReq"
|
||||
:value="'LIGHTNING:' + receive.paymentReq.toUpperCase()"
|
||||
></lnbits-qrcode>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="utils.copyText(receive.paymentReq)"
|
||||
>Copy invoice</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
<div v-else class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h3 class="q-my-none q-pa-lg" v-text="eventErrorLabel"></h3>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,13 +1,5 @@
|
|||
const mapEvents = function (obj) {
|
||||
obj.date = LNbits.utils.formatTimestamp(obj.time)
|
||||
obj.fsat = new Intl.NumberFormat(window.g.locale).format(obj.price_per_ticket)
|
||||
obj.displayUrl = ['/events/', obj.id].join('')
|
||||
return obj
|
||||
}
|
||||
|
||||
window.app = Vue.createApp({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
window.PageEvents = {
|
||||
template: '#page-events',
|
||||
data() {
|
||||
return {
|
||||
events: [],
|
||||
|
|
@ -105,6 +97,7 @@ window.app = Vue.createApp({
|
|||
formDialog: {
|
||||
show: false,
|
||||
data: {
|
||||
currency: 'sats',
|
||||
extra: {
|
||||
promo_codes: []
|
||||
}
|
||||
|
|
@ -118,18 +111,15 @@ window.app = Vue.createApp({
|
|||
.request(
|
||||
'GET',
|
||||
'/events/api/v1/tickets?all_wallets=true',
|
||||
this.g.user.wallets[0].inkey
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
.then(response => {
|
||||
this.tickets = response.data
|
||||
.map(function (obj) {
|
||||
return mapEvents(obj)
|
||||
})
|
||||
.filter(e => e.paid)
|
||||
this.tickets = response.data.filter(e => e.paid)
|
||||
})
|
||||
},
|
||||
deleteTicket(ticketId) {
|
||||
const tickets = _.findWhere(this.tickets, {id: ticketId})
|
||||
const wallet = _.findWhere(this.g.user.wallets, {id: tickets.wallet})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this ticket')
|
||||
|
|
@ -138,16 +128,14 @@ window.app = Vue.createApp({
|
|||
.request(
|
||||
'DELETE',
|
||||
'/events/api/v1/tickets/' + ticketId,
|
||||
_.findWhere(this.g.user.wallets, {id: tickets.wallet}).inkey
|
||||
wallet.adminkey
|
||||
)
|
||||
.then(response => {
|
||||
this.tickets = _.reject(this.tickets, function (obj) {
|
||||
return obj.id == ticketId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
})
|
||||
},
|
||||
exportticketsCSV() {
|
||||
|
|
@ -161,9 +149,7 @@ window.app = Vue.createApp({
|
|||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(response => {
|
||||
this.events = response.data.map(obj => {
|
||||
return mapEvents(obj)
|
||||
})
|
||||
this.events = response.data
|
||||
this.checkCanceledEvents()
|
||||
})
|
||||
},
|
||||
|
|
@ -190,6 +176,7 @@ window.app = Vue.createApp({
|
|||
this.formDialog.data = {...data}
|
||||
} else {
|
||||
this.formDialog.data = {
|
||||
currency: 'sats',
|
||||
extra: {
|
||||
conditional: false,
|
||||
min_tickets: 1,
|
||||
|
|
@ -212,7 +199,7 @@ window.app = Vue.createApp({
|
|||
LNbits.api
|
||||
.request('POST', '/events/api/v1/events', wallet.adminkey, data)
|
||||
.then(response => {
|
||||
this.events.push(mapEvents(response.data))
|
||||
this.events.push(response.data)
|
||||
this.resetEventDialog()
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
|
|
@ -233,7 +220,7 @@ window.app = Vue.createApp({
|
|||
this.events = _.reject(this.events, function (obj) {
|
||||
return obj.id == data.id
|
||||
})
|
||||
this.events.push(mapEvents(response.data))
|
||||
this.events.push(response.data)
|
||||
this.resetEventDialog()
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
|
|
@ -255,7 +242,7 @@ window.app = Vue.createApp({
|
|||
return obj.id == eventsId
|
||||
})
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError(error))
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
})
|
||||
},
|
||||
exporteventsCSV() {
|
||||
|
|
@ -279,9 +266,7 @@ window.app = Vue.createApp({
|
|||
message: `Event ${ev.name} has been canceled and refunds have been issued.`,
|
||||
icon: null
|
||||
})
|
||||
this.events = this.events.map(e =>
|
||||
e.id === ev.id ? mapEvents(data) : e
|
||||
)
|
||||
this.events = this.events.map(e => (e.id === ev.id ? data : e))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -290,7 +275,11 @@ window.app = Vue.createApp({
|
|||
if (this.g.user.wallets.length) {
|
||||
this.getTickets()
|
||||
this.getEvents()
|
||||
this.currencies = await LNbits.api.getCurrencies()
|
||||
if (this.g.allowedCurrencies && this.g.allowedCurrencies.length > 0) {
|
||||
this.currencies = ['sats', ...this.g.allowedCurrencies]
|
||||
} else {
|
||||
this.currencies = ['sats', ...this.g.currencies]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
511
static/js/index.vue
Normal file
511
static/js/index.vue
Normal file
|
|
@ -0,0 +1,511 @@
|
|||
<template id="page-events">
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="primary" @click="openEventDialog"
|
||||
>New Event</q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Events</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exporteventsCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:rows="events"
|
||||
row-key="id"
|
||||
:columns="eventsTable.columns"
|
||||
v-model:pagination="eventsTable.pagination"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th auto-width></q-th>
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<span v-text="col.label"></span>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
size="sm"
|
||||
color="accent"
|
||||
round
|
||||
dense
|
||||
@click="props.expand = !props.expand"
|
||||
:icon="props.expand ? 'expand_less' : 'expand_more'"
|
||||
/>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="link"
|
||||
:color="$q.dark.isActive ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="'/events/' + props.row.id"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="how_to_reg"
|
||||
:color="$q.dark.isActive ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="'/events/register/' + props.row.id"
|
||||
target="_blank"
|
||||
class="q-ml-xs"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="updateformDialog(props.row.id)"
|
||||
icon="edit"
|
||||
color="light-blue"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteEvent(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
class="q-ml-xs"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<span v-text="col.value"></span>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
<q-tr v-show="props.expand" :props="props">
|
||||
<q-td colspan="100%">
|
||||
<div class="q-pa-md">
|
||||
<div class="text-subtitle1 q-mb-md">Promo codes</div>
|
||||
<div class="column">
|
||||
<div
|
||||
v-if="props.row.extra.promo_codes.length == 0"
|
||||
class="text-caption"
|
||||
>
|
||||
No promo codes for this event.
|
||||
</div>
|
||||
<div
|
||||
v-for="(code, index) in props.row.extra.promo_codes"
|
||||
:key="index"
|
||||
class="row items-center q-col-gutter-sm q-mb-sm"
|
||||
>
|
||||
<div class="col-auto">
|
||||
<q-chip
|
||||
square
|
||||
size="md"
|
||||
clickable
|
||||
@click="utils.copyText(code.code.toUpperCase())"
|
||||
>
|
||||
<q-avatar
|
||||
icon="bookmark"
|
||||
:color="code.active ? 'green' : 'grey'"
|
||||
text-color="white"
|
||||
></q-avatar>
|
||||
<span v-text="code.code.toUpperCase()"></span>
|
||||
</q-chip>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
Discount:
|
||||
<span v-text="code.discount_percent"></span>%
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
Status:
|
||||
<span
|
||||
:class="code.active ? 'text-green' : 'text-grey'"
|
||||
v-text="code.active ? 'Active' : 'Inactive'"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Tickets</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportticketsCSV"
|
||||
>Export to CSV</q-btn
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:rows="tickets"
|
||||
row-key="id"
|
||||
:columns="ticketsTable.columns"
|
||||
v-model:pagination="ticketsTable.pagination"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<span v-text="col.label"></span>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="local_activity"
|
||||
:color="$q.dark.isActive ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="'/events/ticket/' + props.row.id"
|
||||
target="_blank"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<span v-text="col.value"></span>
|
||||
</q-td>
|
||||
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteTicket(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 ellipsis q-my-none">
|
||||
<span v-text="SITE_TITLE"></span>
|
||||
Events extension
|
||||
</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="Info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h5 class="text-subtitle1 q-my-none">
|
||||
Events: Sell and register ticket waves for an event
|
||||
</h5>
|
||||
<p>
|
||||
Events allows you to make a wave of tickets for an event,
|
||||
each ticket is in the form of a unique QRcode, which the
|
||||
user presents at registration. Events comes with a shareable
|
||||
ticket scanner, which can be used to register attendees.<br />
|
||||
<small>
|
||||
Created by,
|
||||
<a class="text-secondary" href="https://github.com/benarc"
|
||||
>Ben Arc</a
|
||||
>
|
||||
</small>
|
||||
</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-btn
|
||||
flat
|
||||
label="Swagger API"
|
||||
type="a"
|
||||
href="../docs#/events"
|
||||
></q-btn>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="formDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendEventData" class="q-gutter-md">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.name"
|
||||
type="name"
|
||||
label="Title of event "
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col q-pl-sm">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="formDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
>
|
||||
</q-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.info"
|
||||
type="textarea"
|
||||
label="Info about the event"
|
||||
hint="Markdown supported"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.banner"
|
||||
type="url"
|
||||
label="Image URL"
|
||||
hint="Optional banner image to display on the event page"
|
||||
></q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<div class="col-4">Ticket closing date</div>
|
||||
<div class="col-8">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.closing_date"
|
||||
type="date"
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">Event begins</div>
|
||||
<div class="col-8">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.event_start_date"
|
||||
type="date"
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-4">Event ends</div>
|
||||
<div class="col-8">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.event_end_date"
|
||||
type="date"
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
v-model="formDialog.data.currency"
|
||||
type="text"
|
||||
label="Unit"
|
||||
:options="currencies"
|
||||
></q-select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="formDialog.data.amount_tickets"
|
||||
type="number"
|
||||
label="Amount of tickets "
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="formDialog.data.price_per_ticket"
|
||||
type="number"
|
||||
:label="'Price (' + formDialog.data.currency + ') *'"
|
||||
:step="formDialog.data.currency != 'sats' ? '0.01' : '1'"
|
||||
:mask="formDialog.data.currency != 'sats' ? '#.##' : '#'"
|
||||
fill-mask="0"
|
||||
reverse-fill-mask
|
||||
:disable="formDialog.data.currency == null"
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<q-expansion-item
|
||||
group="advanced"
|
||||
icon="settings"
|
||||
label="Advanced options"
|
||||
>
|
||||
<div class="row q-mt-lg">
|
||||
<div class="text-subtitle1 q-mb-md">Conditional Events</div>
|
||||
<div class="text-caption">
|
||||
Make this event conditional if
|
||||
<strong>minimum tickets</strong> are sold. User will be asked to
|
||||
provide a Lightning Address or LNURL pay for refunds.
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<q-toggle
|
||||
v-model="formDialog.data.extra.conditional"
|
||||
label="Conditional Event"
|
||||
left-label
|
||||
></q-toggle>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.number="formDialog.data.extra.min_tickets"
|
||||
type="number"
|
||||
label="Minimum Tickets"
|
||||
:disable="!formDialog.data.extra.conditional"
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator class="q-my-md"></q-separator>
|
||||
<div class="text-subtitle1 q-mb-md">Promo Codes</div>
|
||||
<div class="text-caption">
|
||||
Allow users to enter a promo code for discounts.
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(code, index) in formDialog.data.extra.promo_codes"
|
||||
:key="index"
|
||||
class="row q-col-gutter-sm q-mt-md"
|
||||
>
|
||||
<q-input
|
||||
class="col-8"
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.extra.promo_codes[index].code"
|
||||
type="text"
|
||||
label="Promo Code"
|
||||
>
|
||||
<template v-slot:before>
|
||||
<q-checkbox
|
||||
left-label
|
||||
v-model="formDialog.data.extra.promo_codes[index].active"
|
||||
checked-icon="radio_button_checked"
|
||||
unchecked-icon="radio_button_unchecked"
|
||||
></q-checkbox>
|
||||
<q-tooltip>
|
||||
<span
|
||||
v-text="
|
||||
formDialog.data.extra.promo_codes[index].active
|
||||
? 'Active'
|
||||
: 'Inactive'
|
||||
"
|
||||
></span>
|
||||
</q-tooltip>
|
||||
</template>
|
||||
</q-input>
|
||||
<q-input
|
||||
class="col-4"
|
||||
filled
|
||||
dense
|
||||
v-model.number="
|
||||
formDialog.data.extra.promo_codes[index].discount_percent
|
||||
"
|
||||
type="number"
|
||||
label="Discount (%)"
|
||||
min="0"
|
||||
max="100"
|
||||
>
|
||||
<template v-slot:after>
|
||||
<q-btn
|
||||
round
|
||||
dense
|
||||
flat
|
||||
icon="delete"
|
||||
@click="formDialog.data.extra.promo_codes.splice(index, 1)"
|
||||
></q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 q-mt-md">
|
||||
<q-btn
|
||||
@click="
|
||||
formDialog.data.extra.promo_codes.push({
|
||||
code: '',
|
||||
discount_percent: 0,
|
||||
active: true
|
||||
})
|
||||
"
|
||||
>Add Promo Code</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="formDialog.data.id"
|
||||
unelevated
|
||||
color="primary"
|
||||
type="submit"
|
||||
>Update Event</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="
|
||||
formDialog.data.wallet == null ||
|
||||
formDialog.data.name == null ||
|
||||
formDialog.data.info == null ||
|
||||
formDialog.data.closing_date == null ||
|
||||
formDialog.data.event_start_date == null ||
|
||||
formDialog.data.event_end_date == null ||
|
||||
formDialog.data.amount_tickets == null ||
|
||||
formDialog.data.price_per_ticket == null
|
||||
"
|
||||
type="submit"
|
||||
>Create Event</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,28 +1,22 @@
|
|||
const mapEvents = function (obj) {
|
||||
obj.date = Quasar.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
||||
obj.displayUrl = ['/events/', obj.id].join('')
|
||||
return obj
|
||||
}
|
||||
|
||||
window.app = Vue.createApp({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
window.PageEventsRegister = {
|
||||
template: '#page-events-register',
|
||||
data() {
|
||||
return {
|
||||
tickets: [],
|
||||
ticketsTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
{
|
||||
name: 'registered',
|
||||
align: 'left',
|
||||
label: 'Registered',
|
||||
field: 'registered'
|
||||
},
|
||||
{
|
||||
name: 'paid',
|
||||
align: 'left',
|
||||
label: 'Paid',
|
||||
field: 'paid'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
|
|
@ -49,30 +43,26 @@ window.app = Vue.createApp({
|
|||
this.sendCamera.show = false
|
||||
const value = res[0].rawValue.split('//')[1]
|
||||
LNbits.api
|
||||
.request('GET', `/events/api/v1/register/ticket/${value}`)
|
||||
.request('PUT', `/events/api/v1/tickets/register/${value}`)
|
||||
.then(() => {
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Registered!'
|
||||
})
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 2000)
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
getEventTickets() {
|
||||
LNbits.api
|
||||
.request('GET', `/events/api/v1/eventtickets/${event_id}`)
|
||||
.request('GET', `/events/api/v1/events/${this.eventId}/tickets`)
|
||||
.then(response => {
|
||||
this.tickets = response.data.map(obj => {
|
||||
return mapEvents(obj)
|
||||
})
|
||||
this.tickets = response.data
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.eventId = this.$route.params.id
|
||||
this.getEventTickets()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
64
static/js/register.vue
Normal file
64
static/js/register.vue
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<template id="page-events-register">
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<center>
|
||||
<h3 class="q-my-none">Registration</h3>
|
||||
<br />
|
||||
|
||||
<br />
|
||||
|
||||
<q-btn unelevated color="primary" @click="showCamera" size="xl"
|
||||
>Scan ticket</q-btn
|
||||
>
|
||||
</center>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:rows="tickets"
|
||||
row-key="id"
|
||||
:columns="ticketsTable.columns"
|
||||
v-model:pagination="ticketsTable.pagination"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<span v-text="col.label"></span>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<span v-text="col.value"></span>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="sendCamera.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl">
|
||||
<div class="text-center q-mb-lg">
|
||||
<qrcode-stream
|
||||
@detect="decodeQR"
|
||||
class="rounded-borders"
|
||||
></qrcode-stream>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
26
static/js/ticket.js
Normal file
26
static/js/ticket.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
window.PageEventsTicket = {
|
||||
template: '#page-events-ticket',
|
||||
data() {
|
||||
return {
|
||||
ticketId: null,
|
||||
ticketName: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
printWindow() {
|
||||
window.print()
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.ticketId = this.$route.params.id
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/events/api/v1/tickets/${this.ticketId}`
|
||||
)
|
||||
this.ticketName = data.ticket_name
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
27
static/js/ticket.vue
Normal file
27
static/js/ticket.vue
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<template id="page-events-ticket">
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<center>
|
||||
<h3 class="q-my-none">Ticket</h3>
|
||||
<br />
|
||||
<h5 class="q-my-none">
|
||||
Bookmark, print or screenshot this page,<br />
|
||||
and present it for registration!
|
||||
</h5>
|
||||
<br />
|
||||
<lnbits-qrcode
|
||||
:value="`ticket://${ticketId}`"
|
||||
:options="{width: 500}"
|
||||
></lnbits-qrcode>
|
||||
<br />
|
||||
<q-btn @click="printWindow" color="grey" class="q-ml-auto">
|
||||
<q-icon left size="3em" name="print"></q-icon> Print</q-btn
|
||||
>
|
||||
</center>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
26
static/routes.json
Normal file
26
static/routes.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
[
|
||||
{
|
||||
"path": "/events/",
|
||||
"name": "PageEvents",
|
||||
"template": "/events/static/js/index.vue",
|
||||
"component": "/events/static/js/index.js"
|
||||
},
|
||||
{
|
||||
"path": "/events/:id",
|
||||
"name": "PageEventsDisplay",
|
||||
"template": "/events/static/js/display.vue",
|
||||
"component": "/events/static/js/display.js"
|
||||
},
|
||||
{
|
||||
"path": "/events/ticket/:id",
|
||||
"name": "PageEventsTicket",
|
||||
"template": "/events/static/js/ticket.vue",
|
||||
"component": "/events/static/js/ticket.js"
|
||||
},
|
||||
{
|
||||
"path": "/events/register/:id",
|
||||
"name": "PageEventsRegister",
|
||||
"template": "/events/static/js/register.vue",
|
||||
"component": "/events/static/js/register.js"
|
||||
}
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue