@frigear-nu/events 0.0.9 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/module.mjs +6 -4
- package/dist/runtime/app/components/EventAdminAddonDialog.d.vue.ts +2 -2
- package/dist/runtime/app/components/EventAdminAddonDialog.vue +3 -4
- package/dist/runtime/app/components/EventAdminAddonDialog.vue.d.ts +2 -2
- package/dist/runtime/app/components/EventAdminAddonGroupDialog.d.vue.ts +2 -2
- package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue +3 -4
- package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue.d.ts +2 -2
- package/dist/runtime/app/components/EventAdminAddonGroups.d.vue.ts +8 -8
- package/dist/runtime/app/components/EventAdminAddonGroups.vue +46 -55
- package/dist/runtime/app/components/EventAdminAddonGroups.vue.d.ts +8 -8
- package/dist/runtime/app/components/EventAdminTicketTypeDialog.d.vue.ts +2 -2
- package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue +3 -4
- package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue.d.ts +2 -2
- package/dist/runtime/app/components/EventCard.vue +7 -12
- package/dist/runtime/app/components/EventFormSlideover.d.vue.ts +13 -0
- package/dist/runtime/app/components/EventFormSlideover.vue +70 -0
- package/dist/runtime/app/components/EventFormSlideover.vue.d.ts +13 -0
- package/dist/runtime/app/components/EventTicketCard.vue +5 -7
- package/dist/runtime/app/components/EventTicketPicker.vue +5 -6
- package/dist/runtime/app/composables/index.d.ts +2 -0
- package/dist/runtime/app/composables/index.js +2 -0
- package/dist/runtime/app/composables/useEventActions.d.ts +26 -0
- package/dist/runtime/app/composables/useEventActions.js +61 -0
- package/dist/runtime/app/composables/useEventLinks.d.ts +12 -0
- package/dist/runtime/app/composables/useEventLinks.js +5 -0
- package/dist/runtime/app/composables/useEvents.d.ts +5 -2
- package/dist/runtime/app/pages/admin-event.vue +33 -21
- package/dist/runtime/app/pages/admin.vue +61 -135
- package/dist/runtime/app/pages/event.d.vue.ts +2 -2
- package/dist/runtime/app/pages/event.vue +113 -152
- package/dist/runtime/app/pages/event.vue.d.ts +2 -2
- package/dist/runtime/app/pages/events.vue +33 -31
- package/dist/runtime/app/pages/tickets.vue +21 -28
- package/dist/runtime/app/tabs/Attendees.d.vue.ts +2 -2
- package/dist/runtime/app/tabs/Attendees.vue +64 -60
- package/dist/runtime/app/tabs/Attendees.vue.d.ts +2 -2
- package/dist/runtime/app/tabs/Details.d.vue.ts +2 -2
- package/dist/runtime/app/tabs/Details.vue +4 -5
- package/dist/runtime/app/tabs/Details.vue.d.ts +2 -2
- package/dist/runtime/app/tabs/Tickets.d.vue.ts +2 -2
- package/dist/runtime/app/tabs/Tickets.vue +92 -99
- package/dist/runtime/app/tabs/Tickets.vue.d.ts +2 -2
- package/dist/runtime/app/utils/admin-event.d.ts +9 -12
- package/dist/runtime/app/utils/admin-event.js +0 -17
- package/dist/runtime/app/widgets/UpcomingEvents.vue +7 -9
- package/dist/runtime/app/widgets/UpcomingEventsStat.vue +5 -6
- package/dist/runtime/i18n/lang/da.json +111 -3
- package/dist/runtime/i18n/lang/en.json +111 -3
- package/dist/runtime/menus.js +2 -4
- package/dist/runtime/server/db/schema.postgresql.d.ts +1110 -0
- package/dist/runtime/server/db/schema.postgresql.js +125 -0
- package/dist/runtime/server/utils/admin.d.ts +5 -5
- package/dist/runtime/utils/index.d.ts +1 -0
- package/dist/runtime/utils/index.js +1 -0
- package/dist/runtime/utils/links.d.ts +21 -0
- package/dist/runtime/utils/links.js +12 -0
- package/package.json +21 -21
|
@@ -5,25 +5,18 @@ const route = useRoute();
|
|
|
5
5
|
const slug = computed(() => String(route.params.slug));
|
|
6
6
|
const { loggedIn } = useUserSession();
|
|
7
7
|
const { event, error, purchase, refresh } = await useEvent(slug);
|
|
8
|
-
const locale = useEventsLocale();
|
|
9
8
|
const tr = useEventsTranslate();
|
|
9
|
+
const format = useFormat();
|
|
10
|
+
const links = useEventLinks();
|
|
10
11
|
const toast = useToast();
|
|
12
|
+
const { busy: buying, run } = useAction();
|
|
11
13
|
if (!event.value) {
|
|
12
14
|
throw createError({ statusCode: error.value?.statusCode ?? 404, statusMessage: t("notFound", "Event not found"), fatal: true });
|
|
13
15
|
}
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const result = await purchase(ticketTypeId, addonIds);
|
|
20
|
-
if (!result.url) toast.add({ title: t("ticketReady", "Your ticket is ready"), color: "success" });
|
|
21
|
-
} catch (e) {
|
|
22
|
-
toast.add({ title: e.statusMessage ?? t("purchaseFailed", "Could not buy the ticket"), color: "error" });
|
|
23
|
-
} finally {
|
|
24
|
-
buying.value = false;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
16
|
+
const onPurchase = (ticketTypeId, addonIds) => run(async () => {
|
|
17
|
+
const result = await purchase(ticketTypeId, addonIds);
|
|
18
|
+
if (!result.url) toast.add({ title: t("ticketReady", "Your ticket is ready"), color: "success", icon: "i-lucide-check" });
|
|
19
|
+
}, { error: t("purchaseFailed", "Could not buy the ticket") });
|
|
27
20
|
const payment = computed(() => route.query.payment);
|
|
28
21
|
const returnedTicket = computed(() => route.query.ticket);
|
|
29
22
|
const paidState = ref(null);
|
|
@@ -41,83 +34,24 @@ onMounted(async () => {
|
|
|
41
34
|
}
|
|
42
35
|
paidState.value = "timeout";
|
|
43
36
|
});
|
|
44
|
-
const
|
|
37
|
+
const hasTicketsPage = !!useRuntimeConfig().public.events?.pages?.tickets;
|
|
45
38
|
useSeoMeta({ title: () => event.value ? tr(event.value.name) : "Event" });
|
|
46
39
|
defineOptions({ name: "EventDetailPage" });
|
|
47
40
|
</script>
|
|
48
41
|
|
|
49
42
|
<template>
|
|
50
|
-
<UContainer
|
|
51
|
-
<
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
color="info"
|
|
56
|
-
variant="subtle"
|
|
57
|
-
:title="t('detail.tickets.payment.verifying.description', 'Confirming your payment\u2026')"
|
|
58
|
-
/>
|
|
59
|
-
<UAlert
|
|
60
|
-
v-else-if="paidState === 'paid'"
|
|
61
|
-
class="mb-6"
|
|
62
|
-
color="success"
|
|
63
|
-
variant="subtle"
|
|
64
|
-
:title="t('paymentReceived', 'Payment received - your ticket is ready.')"
|
|
43
|
+
<UContainer>
|
|
44
|
+
<UPage v-if="event">
|
|
45
|
+
<UPageHeader
|
|
46
|
+
:title="tr(event.name)"
|
|
47
|
+
:description="event.description ? tr(event.description) : void 0"
|
|
65
48
|
>
|
|
66
|
-
<template
|
|
67
|
-
|
|
68
|
-
#actions
|
|
69
|
-
>
|
|
70
|
-
<UButton
|
|
71
|
-
:to="ticketsPath"
|
|
72
|
-
size="xs"
|
|
73
|
-
>
|
|
74
|
-
{{ t("detail.tickets.view", "View ticket") }}
|
|
75
|
-
</UButton>
|
|
49
|
+
<template #headline>
|
|
50
|
+
<UBreadcrumb :items="[{ label: t('title', 'Events'), to: links.events() }, { label: tr(event.name) }]" />
|
|
76
51
|
</template>
|
|
77
|
-
</UAlert>
|
|
78
|
-
<UAlert
|
|
79
|
-
v-else-if="paidState === 'timeout'"
|
|
80
|
-
class="mb-6"
|
|
81
|
-
color="warning"
|
|
82
|
-
variant="subtle"
|
|
83
|
-
:title="t('paymentTimeout', 'We have not received the payment confirmation yet. It will show up under your tickets shortly.')"
|
|
84
|
-
/>
|
|
85
|
-
<UAlert
|
|
86
|
-
v-else-if="payment === 'cancel'"
|
|
87
|
-
class="mb-6"
|
|
88
|
-
color="neutral"
|
|
89
|
-
variant="subtle"
|
|
90
|
-
:title="t('paymentCancelled', 'Payment cancelled. Your reservation is held for a short while; you can pay from your tickets.')"
|
|
91
|
-
/>
|
|
92
|
-
|
|
93
|
-
<UButton
|
|
94
|
-
:to="eventsPath"
|
|
95
|
-
color="neutral"
|
|
96
|
-
variant="link"
|
|
97
|
-
size="sm"
|
|
98
|
-
class="mb-4 -ml-2.5"
|
|
99
|
-
leading-icon="i-lucide-arrow-left"
|
|
100
|
-
:label="t('backToEvents', 'All events')"
|
|
101
|
-
/>
|
|
102
|
-
|
|
103
|
-
<!-- Hero -->
|
|
104
|
-
<div class="relative overflow-hidden rounded-xl ring ring-default">
|
|
105
|
-
<img
|
|
106
|
-
v-if="event.image"
|
|
107
|
-
:src="event.image"
|
|
108
|
-
:alt="tr(event.name)"
|
|
109
|
-
class="aspect-[2/1] w-full object-cover sm:aspect-[3/1]"
|
|
110
|
-
>
|
|
111
|
-
<div
|
|
112
|
-
v-else
|
|
113
|
-
class="gradient-linear-br-teal-purple aspect-[3/1] w-full sm:aspect-[4/1]"
|
|
114
|
-
/>
|
|
115
|
-
</div>
|
|
116
|
-
|
|
117
|
-
<header class="mt-8 space-y-4">
|
|
118
52
|
<div
|
|
119
53
|
v-if="event.requiresMembership"
|
|
120
|
-
class="
|
|
54
|
+
class="mt-3"
|
|
121
55
|
>
|
|
122
56
|
<UBadge
|
|
123
57
|
color="warning"
|
|
@@ -126,41 +60,72 @@ defineOptions({ name: "EventDetailPage" });
|
|
|
126
60
|
:label="t('members', 'Members')"
|
|
127
61
|
/>
|
|
128
62
|
</div>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
<
|
|
133
|
-
v-if="
|
|
134
|
-
|
|
63
|
+
</UPageHeader>
|
|
64
|
+
|
|
65
|
+
<UPageBody>
|
|
66
|
+
<UAlert
|
|
67
|
+
v-if="paidState === 'waiting'"
|
|
68
|
+
color="info"
|
|
69
|
+
variant="subtle"
|
|
70
|
+
:title="t('detail.tickets.payment.verifying.description', 'Confirming your payment\u2026')"
|
|
71
|
+
/>
|
|
72
|
+
<UAlert
|
|
73
|
+
v-else-if="paidState === 'paid'"
|
|
74
|
+
color="success"
|
|
75
|
+
variant="subtle"
|
|
76
|
+
:title="t('paymentReceived', 'Payment received - your ticket is ready.')"
|
|
135
77
|
>
|
|
136
|
-
|
|
137
|
-
|
|
78
|
+
<template
|
|
79
|
+
v-if="hasTicketsPage"
|
|
80
|
+
#actions
|
|
81
|
+
>
|
|
82
|
+
<UButton
|
|
83
|
+
:to="links.tickets()"
|
|
84
|
+
size="xs"
|
|
85
|
+
>
|
|
86
|
+
{{ t("detail.tickets.view", "View ticket") }}
|
|
87
|
+
</UButton>
|
|
88
|
+
</template>
|
|
89
|
+
</UAlert>
|
|
90
|
+
<UAlert
|
|
91
|
+
v-else-if="paidState === 'timeout'"
|
|
92
|
+
color="warning"
|
|
93
|
+
variant="subtle"
|
|
94
|
+
:title="t('paymentTimeout', 'We have not received the payment confirmation yet. It will show up under your tickets shortly.')"
|
|
95
|
+
/>
|
|
96
|
+
<UAlert
|
|
97
|
+
v-else-if="payment === 'cancel'"
|
|
98
|
+
color="neutral"
|
|
99
|
+
variant="subtle"
|
|
100
|
+
:title="t('paymentCancelled', 'Payment cancelled. Your reservation is held for a short while; you can pay from your tickets.')"
|
|
101
|
+
/>
|
|
102
|
+
|
|
103
|
+
<!-- Hero -->
|
|
104
|
+
<div class="ring-default relative overflow-hidden rounded-lg ring">
|
|
105
|
+
<img
|
|
106
|
+
v-if="event.image"
|
|
107
|
+
:src="event.image"
|
|
108
|
+
:alt="tr(event.name)"
|
|
109
|
+
class="aspect-[2/1] w-full object-cover sm:aspect-[3/1]"
|
|
110
|
+
>
|
|
111
|
+
<div
|
|
112
|
+
v-else
|
|
113
|
+
class="gradient-linear-br-teal-purple aspect-[3/1] w-full sm:aspect-[4/1]"
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
138
116
|
|
|
139
|
-
<div class="grid gap-3
|
|
140
|
-
<div class="flex items-start gap-3 rounded-
|
|
117
|
+
<div class="grid gap-3 sm:grid-cols-2">
|
|
118
|
+
<div class="bg-elevated/50 ring-default flex items-start gap-3 rounded-lg p-4 ring">
|
|
141
119
|
<UIcon
|
|
142
120
|
name="i-lucide-calendar"
|
|
143
|
-
class="mt-0.5 size-5 shrink-0
|
|
121
|
+
class="text-primary mt-0.5 size-5 shrink-0"
|
|
144
122
|
/>
|
|
145
123
|
<div class="min-w-0">
|
|
146
|
-
<div class="text-xs font-semibold uppercase tracking-wide
|
|
124
|
+
<div class="text-muted text-xs font-semibold uppercase tracking-wide">
|
|
147
125
|
{{ t("detail.when", "When?") }}
|
|
148
126
|
</div>
|
|
149
|
-
<div class="font-medium
|
|
150
|
-
|
|
151
|
-
:datetime="event.startsAt"
|
|
152
|
-
:locale="locale"
|
|
153
|
-
date-style="full"
|
|
154
|
-
time-style="short"
|
|
155
|
-
/>
|
|
156
|
-
<template v-if="event.endsAt">
|
|
157
|
-
–
|
|
158
|
-
<NuxtTime
|
|
159
|
-
:datetime="event.endsAt"
|
|
160
|
-
:locale="locale"
|
|
161
|
-
time-style="short"
|
|
162
|
-
/>
|
|
163
|
-
</template>
|
|
127
|
+
<div class="text-highlighted font-medium">
|
|
128
|
+
{{ format.dateRange(event.startsAt, event.endsAt, { dateStyle: "full", timeStyle: "short" }) }}
|
|
164
129
|
</div>
|
|
165
130
|
</div>
|
|
166
131
|
</div>
|
|
@@ -170,23 +135,23 @@ defineOptions({ name: "EventDetailPage" });
|
|
|
170
135
|
:href="event.locationUrl || void 0"
|
|
171
136
|
:target="event.locationUrl ? '_blank' : void 0"
|
|
172
137
|
:rel="event.locationUrl ? 'noopener' : void 0"
|
|
173
|
-
class="flex items-start gap-3 rounded-
|
|
174
|
-
:class="event.locationUrl ? '
|
|
138
|
+
class="bg-elevated/50 ring-default flex items-start gap-3 rounded-lg p-4 ring"
|
|
139
|
+
:class="event.locationUrl ? 'hover:bg-elevated transition' : ''"
|
|
175
140
|
>
|
|
176
141
|
<UIcon
|
|
177
142
|
name="i-lucide-map-pin"
|
|
178
|
-
class="mt-0.5 size-5 shrink-0
|
|
143
|
+
class="text-primary mt-0.5 size-5 shrink-0"
|
|
179
144
|
/>
|
|
180
145
|
<div class="min-w-0">
|
|
181
|
-
<div class="text-xs font-semibold uppercase tracking-wide
|
|
146
|
+
<div class="text-muted text-xs font-semibold uppercase tracking-wide">
|
|
182
147
|
{{ t("detail.where", "Where?") }}
|
|
183
148
|
</div>
|
|
184
|
-
<div class="flex items-center gap-1.5 font-medium
|
|
149
|
+
<div class="text-highlighted flex items-center gap-1.5 font-medium">
|
|
185
150
|
{{ tr(event.location) }}
|
|
186
151
|
<UIcon
|
|
187
152
|
v-if="event.locationUrl"
|
|
188
153
|
name="i-lucide-external-link"
|
|
189
|
-
class="size-4
|
|
154
|
+
class="text-muted size-4"
|
|
190
155
|
/>
|
|
191
156
|
</div>
|
|
192
157
|
</div>
|
|
@@ -205,46 +170,42 @@ defineOptions({ name: "EventDetailPage" });
|
|
|
205
170
|
:label="t('detail.tickets.view', 'Show tickets')"
|
|
206
171
|
/>
|
|
207
172
|
</div>
|
|
208
|
-
</header>
|
|
209
173
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
174
|
+
<div class="grid gap-10 lg:grid-cols-[2fr_1fr] lg:items-start">
|
|
175
|
+
<!-- Body renders as MDC when the host has it (Nuxt Content), else plain text; override via the `body` slot. -->
|
|
176
|
+
<div>
|
|
177
|
+
<slot
|
|
178
|
+
name="body"
|
|
179
|
+
:event="event"
|
|
180
|
+
:body="tr(event.body ?? event.description)"
|
|
181
|
+
>
|
|
182
|
+
<MDC
|
|
183
|
+
v-if="hasMdc"
|
|
184
|
+
:value="tr(event.body ?? event.description)"
|
|
185
|
+
class="prose text-muted max-w-none"
|
|
186
|
+
/>
|
|
187
|
+
<div
|
|
188
|
+
v-else
|
|
189
|
+
class="text-muted whitespace-pre-line"
|
|
190
|
+
>
|
|
191
|
+
{{ tr(event.body ?? event.description) }}
|
|
192
|
+
</div>
|
|
193
|
+
</slot>
|
|
194
|
+
</div>
|
|
195
|
+
<SectionCard
|
|
196
|
+
id="tickets"
|
|
197
|
+
:title="t('detail.tickets.title', 'Tickets')"
|
|
198
|
+
class="scroll-mt-24 lg:sticky lg:top-24"
|
|
217
199
|
>
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
:
|
|
221
|
-
|
|
200
|
+
<EventTicketPicker
|
|
201
|
+
:ticket-types="event.ticketTypes"
|
|
202
|
+
:logged-in="loggedIn"
|
|
203
|
+
:loading="buying"
|
|
204
|
+
@purchase="onPurchase"
|
|
222
205
|
/>
|
|
223
|
-
|
|
224
|
-
v-else
|
|
225
|
-
class="whitespace-pre-line text-muted"
|
|
226
|
-
>
|
|
227
|
-
{{ tr(event.body ?? event.description) }}
|
|
228
|
-
</div>
|
|
229
|
-
</slot>
|
|
206
|
+
</SectionCard>
|
|
230
207
|
</div>
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
:ui="{ root: 'rounded-xl shadow-sm scroll-mt-24 lg:sticky lg:top-24' }"
|
|
234
|
-
>
|
|
235
|
-
<template #header>
|
|
236
|
-
<h2 class="text-lg font-semibold">
|
|
237
|
-
{{ t("detail.tickets.title", "Tickets") }}
|
|
238
|
-
</h2>
|
|
239
|
-
</template>
|
|
240
|
-
<EventTicketPicker
|
|
241
|
-
:ticket-types="event.ticketTypes"
|
|
242
|
-
:logged-in="loggedIn"
|
|
243
|
-
:loading="buying"
|
|
244
|
-
@purchase="onPurchase"
|
|
245
|
-
/>
|
|
246
|
-
</UCard>
|
|
247
|
-
</div>
|
|
248
|
-
</template>
|
|
208
|
+
</UPageBody>
|
|
209
|
+
</UPage>
|
|
249
210
|
</UContainer>
|
|
250
211
|
</template>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
declare var
|
|
1
|
+
declare var __VLS_91: {
|
|
2
2
|
event: any;
|
|
3
3
|
body: any;
|
|
4
4
|
};
|
|
5
5
|
type __VLS_Slots = {} & {
|
|
6
|
-
body?: (props: typeof
|
|
6
|
+
body?: (props: typeof __VLS_91) => any;
|
|
7
7
|
};
|
|
8
8
|
declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
9
9
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -1,45 +1,47 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
defineOptions({ name: "EventsListPage" });
|
|
2
3
|
const t = useModuleT("events");
|
|
3
4
|
const { grouped } = await useEvents();
|
|
4
|
-
const
|
|
5
|
-
const eventTo = (slug) => (pages?.event || "/events/:slug").replace(":slug", encodeURIComponent(slug));
|
|
5
|
+
const links = useEventLinks();
|
|
6
6
|
const sections = computed(() => [
|
|
7
7
|
{ title: t("ongoing.title", "Happening now"), events: grouped.value.ongoing },
|
|
8
8
|
{ title: t("upcoming.title", "Upcoming"), events: grouped.value.upcoming },
|
|
9
9
|
{ title: t("past.title", "Past events"), events: grouped.value.past }
|
|
10
10
|
].filter((section) => section.events.length));
|
|
11
|
-
|
|
11
|
+
useSeoMeta({ title: () => t("title", "Events") });
|
|
12
12
|
</script>
|
|
13
13
|
|
|
14
14
|
<template>
|
|
15
|
-
<UContainer
|
|
16
|
-
<
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
</p>
|
|
27
|
-
<section
|
|
28
|
-
v-for="section in sections"
|
|
29
|
-
:key="section.title"
|
|
30
|
-
class="mb-12"
|
|
31
|
-
>
|
|
32
|
-
<h2 class="mb-4 text-xl font-semibold tracking-tight text-highlighted">
|
|
33
|
-
{{ section.title }}
|
|
34
|
-
</h2>
|
|
35
|
-
<div class="grid gap-6 md:grid-cols-2">
|
|
36
|
-
<EventCard
|
|
37
|
-
v-for="item in section.events"
|
|
38
|
-
:key="item.id"
|
|
39
|
-
:event="item"
|
|
40
|
-
:to="eventTo(item.slug)"
|
|
15
|
+
<UContainer>
|
|
16
|
+
<UPage>
|
|
17
|
+
<UPageHeader
|
|
18
|
+
:title="t('title', 'Events')"
|
|
19
|
+
:description="t('listDescription', 'Find an event and get your ticket.')"
|
|
20
|
+
/>
|
|
21
|
+
<UPageBody>
|
|
22
|
+
<UEmpty
|
|
23
|
+
v-if="!sections.length"
|
|
24
|
+
icon="i-lucide-calendar"
|
|
25
|
+
:title="t('empty', 'There are no events right now.')"
|
|
41
26
|
/>
|
|
42
|
-
|
|
43
|
-
|
|
27
|
+
<section
|
|
28
|
+
v-for="section in sections"
|
|
29
|
+
:key="section.title"
|
|
30
|
+
class="space-y-4"
|
|
31
|
+
>
|
|
32
|
+
<h2 class="text-highlighted text-lg font-semibold">
|
|
33
|
+
{{ section.title }}
|
|
34
|
+
</h2>
|
|
35
|
+
<div class="grid gap-6 md:grid-cols-2">
|
|
36
|
+
<EventCard
|
|
37
|
+
v-for="item in section.events"
|
|
38
|
+
:key="item.id"
|
|
39
|
+
:event="item"
|
|
40
|
+
:to="links.event(item.slug)"
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
</section>
|
|
44
|
+
</UPageBody>
|
|
45
|
+
</UPage>
|
|
44
46
|
</UContainer>
|
|
45
47
|
</template>
|
|
@@ -1,37 +1,30 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
defineOptions({ name: "EventTicketsPage" });
|
|
2
3
|
const t = useModuleT("events");
|
|
3
4
|
const { tickets, pay } = useMyTickets();
|
|
4
|
-
const
|
|
5
|
-
const eventTo = (slug) => (pages?.event || "/events/:slug").replace(":slug", encodeURIComponent(slug));
|
|
6
|
-
const AccountHeader = resolveComponent("AccountHeader");
|
|
7
|
-
const hasAccountHeader = typeof AccountHeader !== "string";
|
|
8
|
-
defineOptions({ name: "EventTicketsPage" });
|
|
5
|
+
const links = useEventLinks();
|
|
9
6
|
</script>
|
|
10
7
|
|
|
11
8
|
<template>
|
|
12
|
-
<div>
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
<div class="flex flex-col gap-4">
|
|
10
|
+
<UEmpty
|
|
11
|
+
v-if="!tickets.length"
|
|
12
|
+
icon="i-lucide-ticket"
|
|
13
|
+
:title="t('noTicketsTitle', 'No tickets yet')"
|
|
14
|
+
:description="t('noTickets', 'You do not have any tickets yet.')"
|
|
15
|
+
:actions="[{ label: t('browseEvents', 'Browse events'), to: links.events(), icon: 'i-lucide-calendar' }]"
|
|
16
|
+
/>
|
|
17
|
+
<div
|
|
18
|
+
v-else
|
|
19
|
+
class="grid gap-4 md:grid-cols-2"
|
|
20
|
+
>
|
|
21
|
+
<EventTicketCard
|
|
22
|
+
v-for="ticket in tickets"
|
|
23
|
+
:key="ticket.id"
|
|
24
|
+
:ticket="ticket"
|
|
25
|
+
:to="links.event(ticket.eventSlug)"
|
|
26
|
+
@pay="pay"
|
|
19
27
|
/>
|
|
20
|
-
|
|
21
|
-
v-if="!tickets.length"
|
|
22
|
-
class="text-muted"
|
|
23
|
-
>
|
|
24
|
-
{{ t("noTickets", "You do not have any tickets yet.") }}
|
|
25
|
-
</p>
|
|
26
|
-
<div class="grid gap-4 md:grid-cols-2">
|
|
27
|
-
<EventTicketCard
|
|
28
|
-
v-for="ticket in tickets"
|
|
29
|
-
:key="ticket.id"
|
|
30
|
-
:ticket="ticket"
|
|
31
|
-
:to="eventTo(ticket.eventSlug)"
|
|
32
|
-
@pay="pay"
|
|
33
|
-
/>
|
|
34
|
-
</div>
|
|
35
|
-
</UContainer>
|
|
28
|
+
</div>
|
|
36
29
|
</div>
|
|
37
30
|
</template>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { StaffEvent } from '../utils/admin-event.js';
|
|
2
2
|
type __VLS_Props = {
|
|
3
|
-
event:
|
|
3
|
+
event: StaffEvent;
|
|
4
4
|
};
|
|
5
5
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
6
6
|
declare const _default: typeof __VLS_export;
|
|
@@ -1,88 +1,92 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { useAdminAction } from "../utils/admin-event";
|
|
3
2
|
defineOptions({ name: "EventsAdminAttendeesTab" });
|
|
4
3
|
const props = defineProps({
|
|
5
4
|
event: { type: Object, required: true }
|
|
6
5
|
});
|
|
7
6
|
const t = useModuleT("events");
|
|
8
7
|
const tr = useEventsTranslate();
|
|
8
|
+
const format = useFormat();
|
|
9
9
|
const confirmAction = useConfirmDialog();
|
|
10
|
-
const { run } =
|
|
10
|
+
const { run } = useAction();
|
|
11
11
|
const { data: attendees, refresh } = await useFetch(() => `/api/events/admin/events/${props.event.id}/tickets`, { default: () => [] });
|
|
12
12
|
const typeName = (ticketTypeId) => tr(props.event.ticketTypes.find((item) => item.id === ticketTypeId)?.name);
|
|
13
|
-
const
|
|
13
|
+
const who = (ticket) => ticket.userName || ticket.userEmail || "";
|
|
14
14
|
const ticketAction = async (ticket, action) => {
|
|
15
15
|
const confirmed = await confirmAction({
|
|
16
16
|
title: action === "refund" ? t("admin.confirmRefund", "Refund this ticket?") : t("admin.confirmCancel", "Cancel this ticket?"),
|
|
17
|
-
|
|
17
|
+
description: action === "refund" ? t("admin.confirmRefundDescription", "The full amount goes back to {name} and the ticket is cancelled.", { name: who(ticket) }) : t("admin.confirmCancelDescription", "The ticket of {name} stops working and its seat is freed.", { name: who(ticket) }),
|
|
18
|
+
confirmText: action === "refund" ? t("admin.refund", "Refund ticket") : t("admin.cancelTicket", "Cancel ticket"),
|
|
18
19
|
destructive: true
|
|
19
20
|
});
|
|
20
21
|
if (!confirmed) return;
|
|
21
22
|
return run(async () => {
|
|
22
23
|
await $fetch(`/api/events/admin/tickets/${ticket.id}/${action}`, { method: "POST" });
|
|
23
24
|
await refresh();
|
|
24
|
-
}, action === "refund" ? t("admin.refunded", "Refunded.") : t("admin.cancelled", "Cancelled."));
|
|
25
|
+
}, { success: action === "refund" ? t("admin.refunded", "Refunded.") : t("admin.cancelled", "Cancelled.") });
|
|
25
26
|
};
|
|
26
|
-
const
|
|
27
|
+
const menu = (ticket) => {
|
|
28
|
+
const refund = ticket.status === "paid" && ticket.paymentId ? [{ label: t("admin.refund", "Refund ticket"), icon: "i-lucide-undo-2", onSelect: () => {
|
|
29
|
+
void ticketAction(ticket, "refund");
|
|
30
|
+
} }] : [];
|
|
31
|
+
const cancel = ticket.status === "paid" || ticket.status === "pending" ? [{ label: t("admin.cancelTicket", "Cancel ticket"), icon: "i-lucide-ban", color: "error", onSelect: () => {
|
|
32
|
+
void ticketAction(ticket, "cancel");
|
|
33
|
+
} }] : [];
|
|
34
|
+
return [refund, cancel].filter((group) => group.length);
|
|
35
|
+
};
|
|
36
|
+
const columns = computed(() => [
|
|
27
37
|
{ accessorKey: "userName", header: t("admin.attendee", "Attendee") },
|
|
28
38
|
{ accessorKey: "ticketTypeId", header: t("admin.ticketType", "Ticket") },
|
|
29
39
|
{ accessorKey: "status", header: t("admin.status", "Status") },
|
|
30
40
|
{ accessorKey: "checkedInAt", header: t("admin.checkedIn", "Checked in") },
|
|
31
|
-
{ id: "actions"
|
|
32
|
-
];
|
|
41
|
+
{ id: "actions" }
|
|
42
|
+
]);
|
|
33
43
|
</script>
|
|
34
44
|
|
|
35
45
|
<template>
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
</
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<AdminEmpty
|
|
83
|
-
v-if="!attendees.length"
|
|
84
|
-
icon="i-lucide-users"
|
|
85
|
-
:title="t('admin.noAttendees', 'No tickets yet.')"
|
|
86
|
-
/>
|
|
87
|
-
</AdminSection>
|
|
46
|
+
<SectionCard
|
|
47
|
+
:title="t('admin.attendees', 'Attendees')"
|
|
48
|
+
flush
|
|
49
|
+
>
|
|
50
|
+
<UTable
|
|
51
|
+
:data="attendees"
|
|
52
|
+
:columns="columns"
|
|
53
|
+
:ui="tableUi({ tr: '' })"
|
|
54
|
+
>
|
|
55
|
+
<template #userName-cell="{ row }">
|
|
56
|
+
<span class="text-highlighted font-medium">{{ row.original.userName }}</span>
|
|
57
|
+
<span class="text-muted"> · {{ row.original.userEmail }}</span>
|
|
58
|
+
</template>
|
|
59
|
+
<template #ticketTypeId-cell="{ row }">
|
|
60
|
+
{{ typeName(row.original.ticketTypeId) }}
|
|
61
|
+
</template>
|
|
62
|
+
<template #status-cell="{ row }">
|
|
63
|
+
<UBadge
|
|
64
|
+
variant="subtle"
|
|
65
|
+
:color="statusColor(row.original.status)"
|
|
66
|
+
:label="t(`tickets.status.${row.original.status}`, row.original.status)"
|
|
67
|
+
/>
|
|
68
|
+
</template>
|
|
69
|
+
<template #checkedInAt-cell="{ row }">
|
|
70
|
+
<span v-if="row.original.checkedInAt">{{ format.dateTime(row.original.checkedInAt) }}</span>
|
|
71
|
+
<span
|
|
72
|
+
v-else
|
|
73
|
+
class="text-muted"
|
|
74
|
+
>–</span>
|
|
75
|
+
</template>
|
|
76
|
+
<template #actions-cell="{ row }">
|
|
77
|
+
<RowMenu
|
|
78
|
+
v-if="menu(row.original).length"
|
|
79
|
+
:items="menu(row.original)"
|
|
80
|
+
:subject="who(row.original)"
|
|
81
|
+
/>
|
|
82
|
+
</template>
|
|
83
|
+
<template #empty>
|
|
84
|
+
<UEmpty
|
|
85
|
+
variant="naked"
|
|
86
|
+
icon="i-lucide-users"
|
|
87
|
+
:title="t('admin.noAttendees', 'No tickets yet.')"
|
|
88
|
+
/>
|
|
89
|
+
</template>
|
|
90
|
+
</UTable>
|
|
91
|
+
</SectionCard>
|
|
88
92
|
</template>
|