@frigear-nu/events 0.1.1 → 0.1.3
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.d.mts +105 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +40 -31
- package/dist/runtime/app/components/EventAddonGroupSlideover.d.vue.ts +18 -0
- package/dist/runtime/app/components/EventAddonGroupSlideover.vue +66 -0
- package/dist/runtime/app/components/EventAddonGroupSlideover.vue.d.ts +18 -0
- package/dist/runtime/app/components/EventAddonSlideover.d.vue.ts +20 -0
- package/dist/runtime/app/components/EventAddonSlideover.vue +103 -0
- package/dist/runtime/app/components/EventAddonSlideover.vue.d.ts +20 -0
- package/dist/runtime/app/components/EventAdminAddonGroups.d.vue.ts +1 -9
- package/dist/runtime/app/components/EventAdminAddonGroups.vue +6 -48
- package/dist/runtime/app/components/EventAdminAddonGroups.vue.d.ts +1 -9
- package/dist/runtime/app/components/EventFormSlideover.vue +45 -23
- package/dist/runtime/app/components/EventTicketCard.vue +59 -62
- package/dist/runtime/app/components/EventTicketTypeSlideover.d.vue.ts +20 -0
- package/dist/runtime/app/components/EventTicketTypeSlideover.vue +176 -0
- package/dist/runtime/app/components/EventTicketTypeSlideover.vue.d.ts +20 -0
- package/dist/runtime/app/composables/index.d.ts +2 -0
- package/dist/runtime/app/composables/index.js +1 -0
- package/dist/runtime/app/composables/useEventActions.d.ts +3 -3
- package/dist/runtime/app/composables/useEventActions.js +9 -4
- package/dist/runtime/app/composables/useEventCatalogActions.d.ts +33 -0
- package/dist/runtime/app/composables/useEventCatalogActions.js +107 -0
- package/dist/runtime/app/composables/useEvents.d.ts +7 -6
- package/dist/runtime/app/composables/useEvents.js +17 -10
- package/dist/runtime/app/pages/admin-event.vue +71 -136
- package/dist/runtime/app/pages/admin.vue +91 -29
- package/dist/runtime/app/pages/event.vue +8 -4
- package/dist/runtime/app/pages/tickets.vue +24 -2
- package/dist/runtime/app/tabs/Attendees.vue +78 -39
- package/dist/runtime/app/tabs/Details.d.vue.ts +1 -1
- package/dist/runtime/app/tabs/Details.vue +112 -30
- package/dist/runtime/app/tabs/Details.vue.d.ts +1 -1
- package/dist/runtime/app/tabs/Tickets.vue +105 -132
- package/dist/runtime/app/utils/admin-event.d.ts +53 -38
- package/dist/runtime/app/utils/admin-event.js +14 -0
- package/dist/runtime/app/widgets/UpcomingEvents.vue +2 -1
- package/dist/runtime/app/widgets/UpcomingEventsStat.vue +2 -1
- package/dist/runtime/i18n/lang/da.json +24 -3
- package/dist/runtime/i18n/lang/en.json +24 -3
- package/dist/runtime/server/bootstrap/my-tickets.d.ts +7 -0
- package/dist/runtime/server/{routes/public/tickets-me.get.js → bootstrap/my-tickets.js} +2 -2
- package/dist/runtime/server/routes/admin/addon-group.delete.js +6 -3
- package/dist/runtime/server/routes/admin/addon-group.patch.js +6 -4
- package/dist/runtime/server/routes/admin/addon-groups.post.js +8 -6
- package/dist/runtime/server/routes/admin/addon.delete.js +6 -3
- package/dist/runtime/server/routes/admin/addon.patch.js +8 -5
- package/dist/runtime/server/routes/admin/addons.post.js +8 -6
- package/dist/runtime/server/routes/admin/attendees.get.js +38 -8
- package/dist/runtime/server/routes/{public → admin}/check-in.post.js +4 -3
- package/dist/runtime/server/routes/admin/event-clone.post.js +37 -19
- package/dist/runtime/server/routes/admin/event.delete.js +7 -4
- package/dist/runtime/server/routes/admin/event.get.js +4 -2
- package/dist/runtime/server/routes/admin/event.patch.js +4 -2
- package/dist/runtime/server/routes/admin/events.get.js +36 -3
- package/dist/runtime/server/routes/admin/events.post.js +3 -4
- package/dist/runtime/server/routes/admin/membership-options.get.js +3 -2
- package/dist/runtime/server/routes/admin/ticket-cancel.post.js +7 -8
- package/dist/runtime/server/routes/admin/ticket-refund.post.js +7 -5
- package/dist/runtime/server/routes/admin/ticket-type.delete.js +7 -4
- package/dist/runtime/server/routes/admin/ticket-type.patch.js +4 -2
- package/dist/runtime/server/routes/admin/ticket-types.post.js +10 -7
- package/dist/runtime/server/routes/admin/upcoming.get.js +7 -3
- package/dist/runtime/server/routes/me/ticket-pay.post.js +13 -0
- package/dist/runtime/server/routes/me/ticket.get.js +6 -0
- package/dist/runtime/server/routes/me/tickets.get.js +2 -0
- package/dist/runtime/server/routes/public/event.get.js +5 -2
- package/dist/runtime/server/routes/public/purchase.post.js +3 -1
- package/dist/runtime/server/utils/admin-dto.d.ts +30 -0
- package/dist/runtime/server/utils/admin-dto.js +101 -0
- package/dist/runtime/server/utils/admin-schemas.js +2 -1
- package/dist/runtime/server/utils/admin.d.ts +3 -1
- package/dist/runtime/server/utils/admin.js +20 -6
- package/dist/runtime/server/utils/auth.d.ts +5 -4
- package/dist/runtime/server/utils/auth.js +6 -7
- package/dist/runtime/server/utils/tickets.js +6 -11
- package/dist/runtime/utils/api.d.ts +48 -0
- package/dist/runtime/utils/api.js +51 -0
- package/dist/runtime/utils/index.d.ts +2 -1
- package/dist/runtime/utils/index.js +1 -0
- package/dist/types.d.mts +1 -1
- package/package.json +21 -16
- package/dist/runtime/app/components/EventAdminAddonDialog.d.vue.ts +0 -11
- package/dist/runtime/app/components/EventAdminAddonDialog.vue +0 -112
- package/dist/runtime/app/components/EventAdminAddonDialog.vue.d.ts +0 -11
- package/dist/runtime/app/components/EventAdminAddonGroupDialog.d.vue.ts +0 -11
- package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue +0 -78
- package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue.d.ts +0 -11
- package/dist/runtime/app/components/EventAdminTicketTypeDialog.d.vue.ts +0 -20
- package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue +0 -174
- package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue.d.ts +0 -20
- package/dist/runtime/server/routes/public/ticket-pay.post.js +0 -10
- package/dist/runtime/server/routes/public/ticket.get.js +0 -4
- /package/dist/runtime/server/routes/{public → admin}/check-in.post.d.ts +0 -0
- /package/dist/runtime/server/routes/{public → me}/ticket-pay.post.d.ts +0 -0
- /package/dist/runtime/server/routes/{public → me}/ticket.get.d.ts +0 -0
- /package/dist/runtime/server/routes/{public/tickets-me.get.d.ts → me/tickets.get.d.ts} +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { toValue } from "vue";
|
|
2
|
+
import EventAddonGroupSlideover from "../components/EventAddonGroupSlideover.vue";
|
|
3
|
+
import EventAddonSlideover from "../components/EventAddonSlideover.vue";
|
|
4
|
+
import EventTicketTypeSlideover from "../components/EventTicketTypeSlideover.vue";
|
|
5
|
+
import { eventsApi } from "../../utils/api.js";
|
|
6
|
+
export function useEventCatalogActions(options = {}) {
|
|
7
|
+
const t = useModuleT("events");
|
|
8
|
+
const tr = useEventsTranslate();
|
|
9
|
+
const overlay = useOverlay();
|
|
10
|
+
const confirm = useConfirmDialog();
|
|
11
|
+
const { run } = useAction();
|
|
12
|
+
async function changed(result) {
|
|
13
|
+
if (result) await options.onChanged?.();
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
async function openTicketType(ticketType) {
|
|
17
|
+
const eventId = toValue(options.eventId) ?? "";
|
|
18
|
+
if (!ticketType && !eventId) throw new Error("useEventCatalogActions: pass `eventId` to add ticket types.");
|
|
19
|
+
const saved = await overlay.create(EventTicketTypeSlideover, { destroyOnClose: true }).open({ eventId, ticketType, membership: options.membership?.() });
|
|
20
|
+
return changed(saved || false);
|
|
21
|
+
}
|
|
22
|
+
async function openGroup(ticketType, group) {
|
|
23
|
+
const saved = await overlay.create(EventAddonGroupSlideover, { destroyOnClose: true }).open({ ticketTypeId: ticketType.id, group });
|
|
24
|
+
return changed(saved || false);
|
|
25
|
+
}
|
|
26
|
+
async function openAddon(ticketType, group, addon) {
|
|
27
|
+
const saved = await overlay.create(EventAddonSlideover, { destroyOnClose: true }).open({ groupId: group.id, currency: ticketType.currency, addon });
|
|
28
|
+
return changed(saved || false);
|
|
29
|
+
}
|
|
30
|
+
async function remove(request) {
|
|
31
|
+
const confirmed = await confirm({ title: request.title, description: request.description, confirmText: request.confirmText, destructive: true });
|
|
32
|
+
if (!confirmed) return false;
|
|
33
|
+
const done = await run(() => $fetch(request.url, { method: "DELETE" }).then(() => true), { success: request.success });
|
|
34
|
+
return changed(done === true);
|
|
35
|
+
}
|
|
36
|
+
function removeTicketType(ticketType) {
|
|
37
|
+
const name = tr(ticketType.name);
|
|
38
|
+
return remove({
|
|
39
|
+
url: eventsApi.admin.ticketType(ticketType.id),
|
|
40
|
+
title: t("admin.confirmRemoveType", "Delete {name}?", { name }),
|
|
41
|
+
description: t("admin.confirmRemoveTypeDescription", "Its add-on groups and choices are deleted too. A ticket type with active tickets cannot be deleted."),
|
|
42
|
+
confirmText: t("admin.removeType", "Delete ticket type"),
|
|
43
|
+
success: t("admin.typeDeleted", "Deleted {name}", { name })
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function removeGroup(group) {
|
|
47
|
+
const name = tr(group.title);
|
|
48
|
+
return remove({
|
|
49
|
+
url: eventsApi.admin.addonGroup(group.id),
|
|
50
|
+
title: t("admin.confirmRemoveGroup", "Delete {name}?", { name }),
|
|
51
|
+
description: t("admin.confirmRemoveGroupDescription", "Its choices are deleted too. Tickets already bought keep the choices they made."),
|
|
52
|
+
confirmText: t("admin.removeGroup", "Delete add-on group"),
|
|
53
|
+
success: t("admin.groupDeleted", "Deleted {name}", { name })
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function removeAddon(addon) {
|
|
57
|
+
const name = tr(addon.label);
|
|
58
|
+
return remove({
|
|
59
|
+
url: eventsApi.admin.addon(addon.id),
|
|
60
|
+
title: t("admin.confirmRemoveAddon", "Delete {name}?", { name }),
|
|
61
|
+
description: t("admin.confirmRemoveAddonDescription", "Tickets already bought keep the choice they made."),
|
|
62
|
+
confirmText: t("admin.removeAddon", "Delete choice"),
|
|
63
|
+
success: t("admin.addonDeleted", "Deleted {name}", { name })
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function typeMenu(ticketType) {
|
|
67
|
+
return [
|
|
68
|
+
[
|
|
69
|
+
{ label: t("admin.editType", "Edit ticket type"), icon: "i-lucide-pencil", onSelect: () => {
|
|
70
|
+
void openTicketType(ticketType);
|
|
71
|
+
} },
|
|
72
|
+
{ label: t("admin.addGroup", "Add add-on group"), icon: "i-lucide-plus", onSelect: () => {
|
|
73
|
+
void openGroup(ticketType);
|
|
74
|
+
} }
|
|
75
|
+
],
|
|
76
|
+
[{ label: t("admin.removeType", "Delete ticket type"), icon: "i-lucide-trash-2", color: "error", onSelect: () => {
|
|
77
|
+
void removeTicketType(ticketType);
|
|
78
|
+
} }]
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
function groupMenu(ticketType, group) {
|
|
82
|
+
return [
|
|
83
|
+
[
|
|
84
|
+
{ label: t("admin.editGroup", "Edit add-on group"), icon: "i-lucide-pencil", onSelect: () => {
|
|
85
|
+
void openGroup(ticketType, group);
|
|
86
|
+
} },
|
|
87
|
+
{ label: t("admin.addAddon", "Add choice"), icon: "i-lucide-plus", onSelect: () => {
|
|
88
|
+
void openAddon(ticketType, group);
|
|
89
|
+
} }
|
|
90
|
+
],
|
|
91
|
+
[{ label: t("admin.removeGroup", "Delete add-on group"), icon: "i-lucide-trash-2", color: "error", onSelect: () => {
|
|
92
|
+
void removeGroup(group);
|
|
93
|
+
} }]
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
function addonMenu(ticketType, group, addon) {
|
|
97
|
+
return [
|
|
98
|
+
[{ label: t("admin.editAddon", "Edit choice"), icon: "i-lucide-pencil", onSelect: () => {
|
|
99
|
+
void openAddon(ticketType, group, addon);
|
|
100
|
+
} }],
|
|
101
|
+
[{ label: t("admin.removeAddon", "Delete choice"), icon: "i-lucide-trash-2", color: "error", onSelect: () => {
|
|
102
|
+
void removeAddon(addon);
|
|
103
|
+
} }]
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
return { openTicketType, openGroup, openAddon, removeTicketType, removeGroup, removeAddon, typeMenu, groupMenu, addonMenu };
|
|
107
|
+
}
|
|
@@ -16,12 +16,13 @@ export declare function useEvent(slug: MaybeRefOrGetter<string>): Promise<{
|
|
|
16
16
|
url?: string;
|
|
17
17
|
}>;
|
|
18
18
|
}>;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
/**
|
|
20
|
+
* The signed-in user's tickets. Awaitable: `await useMyTickets()` in a page renders them on the server; a widget can
|
|
21
|
+
* use it without `await` and show a skeleton while `loading`.
|
|
22
|
+
*
|
|
23
|
+
* `tickets` is `[]` until loaded and after an error: check `loading` and `error` before showing an empty state.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useMyTickets(): any;
|
|
25
26
|
/** Locale for translated texts: `@nuxtjs/i18n`'s when installed, otherwise `en`. */
|
|
26
27
|
export declare function useEventsLocale(): any;
|
|
27
28
|
/** The site's default locale: shown for texts that have no translation in the current locale. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { translate } from "../../utils/index.js";
|
|
1
|
+
import { eventsApi, translate } from "../../utils/index.js";
|
|
2
2
|
export async function useEvents() {
|
|
3
3
|
const now = useState("events:now", () => Date.now());
|
|
4
|
-
const { data, status, refresh } = await useFetch(
|
|
4
|
+
const { data, status, refresh } = await useFetch(eventsApi.list, { key: "events:list", default: () => [] });
|
|
5
5
|
const grouped = computed(() => {
|
|
6
6
|
const start = (e) => new Date(e.startsAt).getTime();
|
|
7
7
|
const end = (e) => e.endsAt ? new Date(e.endsAt).getTime() : start(e) + 24 * 36e5;
|
|
@@ -16,13 +16,13 @@ export async function useEvents() {
|
|
|
16
16
|
export async function useEvent(slug) {
|
|
17
17
|
const { loggedIn } = useUserSession();
|
|
18
18
|
const locale = useEventsLocale();
|
|
19
|
-
const { data, status, error, refresh } = await useFetch(() =>
|
|
19
|
+
const { data, status, error, refresh } = await useFetch(() => eventsApi.event(toValue(slug)), {
|
|
20
20
|
key: `events:detail:${toValue(slug)}`,
|
|
21
21
|
// Eligibility (membership, limits) depends on the session; refetch quietly, keeping the data on screen.
|
|
22
22
|
watch: [loggedIn]
|
|
23
23
|
});
|
|
24
24
|
async function purchase(ticketTypeId, addonIds = []) {
|
|
25
|
-
const result = await $fetch(
|
|
25
|
+
const result = await $fetch(eventsApi.purchase(toValue(slug)), {
|
|
26
26
|
method: "POST",
|
|
27
27
|
body: { ticketTypeId, addonIds, locale: locale.value }
|
|
28
28
|
});
|
|
@@ -32,19 +32,26 @@ export async function useEvent(slug) {
|
|
|
32
32
|
}
|
|
33
33
|
return { event: data, status, error, refresh, purchase };
|
|
34
34
|
}
|
|
35
|
+
const MY_TICKETS_TTL_MS = 60 * 1e3;
|
|
35
36
|
export function useMyTickets() {
|
|
36
37
|
const { loggedIn } = useUserSession();
|
|
37
|
-
const
|
|
38
|
+
const fetched = useCachedFetch(eventsApi.me.tickets, {
|
|
38
39
|
key: "events:tickets:mine",
|
|
39
|
-
|
|
40
|
-
watch: false
|
|
41
|
-
default: () => []
|
|
40
|
+
ttl: MY_TICKETS_TTL_MS,
|
|
41
|
+
watch: false
|
|
42
42
|
});
|
|
43
|
+
const { status, error, loading, refresh, clear } = fetched;
|
|
44
|
+
watch(loggedIn, (value) => {
|
|
45
|
+
if (value) refresh();
|
|
46
|
+
else clear();
|
|
47
|
+
});
|
|
48
|
+
const tickets = computed(() => fetched.data.value ?? []);
|
|
43
49
|
async function pay(ticketId) {
|
|
44
|
-
const { url } = await $fetch(
|
|
50
|
+
const { url } = await $fetch(eventsApi.me.pay(ticketId), { method: "POST" });
|
|
45
51
|
await navigateTo(url, { external: true });
|
|
46
52
|
}
|
|
47
|
-
|
|
53
|
+
const api = { tickets, status, error, loading, refresh, pay };
|
|
54
|
+
return Object.assign(fetched.then(() => api), api);
|
|
48
55
|
}
|
|
49
56
|
export function useEventsLocale() {
|
|
50
57
|
const nuxtApp = useNuxtApp();
|
|
@@ -2,162 +2,97 @@
|
|
|
2
2
|
import AttendeesTab from "../tabs/Attendees.vue";
|
|
3
3
|
import DetailsTab from "../tabs/Details.vue";
|
|
4
4
|
import TicketsTab from "../tabs/Tickets.vue";
|
|
5
|
-
import {
|
|
5
|
+
import { eventsApi } from "../../utils/api";
|
|
6
6
|
defineOptions({ name: "EventsAdminEventPage" });
|
|
7
7
|
const t = useModuleT("events");
|
|
8
8
|
const route = useRoute();
|
|
9
9
|
const tr = useEventsTranslate();
|
|
10
|
+
const format = useFormat();
|
|
10
11
|
const links = useEventLinks();
|
|
11
12
|
const actions = useEventActions();
|
|
12
13
|
const hasPublicPage = !!useRuntimeConfig().public.events?.pages?.event;
|
|
13
14
|
const id = computed(() => String(route.params.id));
|
|
14
|
-
const {
|
|
15
|
-
const
|
|
15
|
+
const { data: event, error, refresh } = await useFetch(() => eventsApi.admin.event(id.value));
|
|
16
|
+
const notFound = computed(() => error.value?.statusCode === 404);
|
|
16
17
|
const Tabs = resolveComponent("WidgetTabs");
|
|
17
18
|
const tabs = typeof Tabs === "string" ? void 0 : Tabs;
|
|
18
|
-
const facts =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
});
|
|
25
|
-
watch(event, (value) => {
|
|
26
|
-
if (!value) return;
|
|
27
|
-
Object.assign(facts, {
|
|
28
|
-
status: value.status,
|
|
29
|
-
startsAt: toLocalInput(value.startsAt),
|
|
30
|
-
endsAt: toLocalInput(value.endsAt),
|
|
31
|
-
location: value.location ?? void 0,
|
|
32
|
-
slug: value.slug
|
|
33
|
-
});
|
|
34
|
-
}, { immediate: true });
|
|
35
|
-
const statuses = computed(() => ["draft", "published", "archived"].map((value) => ({ value, label: t(`admin.statuses.${value}`, value.charAt(0).toUpperCase() + value.slice(1)) })));
|
|
36
|
-
const saveFacts = () => run(async () => {
|
|
37
|
-
await $fetch(`/api/events/admin/events/${id.value}`, {
|
|
38
|
-
method: "PATCH",
|
|
39
|
-
body: {
|
|
40
|
-
status: facts.status,
|
|
41
|
-
startsAt: facts.startsAt,
|
|
42
|
-
endsAt: facts.endsAt || null,
|
|
43
|
-
location: facts.location ?? null,
|
|
44
|
-
slug: facts.slug
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
await refresh();
|
|
48
|
-
}, { success: t("admin.saved", "Saved.") });
|
|
19
|
+
const facts = computed(() => event.value ? [
|
|
20
|
+
{ key: "startsAt", label: t("admin.starts", "Starts"), value: format.dateTime(event.value.startsAt) },
|
|
21
|
+
{ key: "endsAt", label: t("admin.ends", "Ends"), value: event.value.endsAt ? format.dateTime(event.value.endsAt) : null },
|
|
22
|
+
{ key: "location", label: t("admin.location", "Location"), value: tr(event.value.location) },
|
|
23
|
+
{ key: "slug", label: t("admin.slug", "URL name"), value: event.value.slug }
|
|
24
|
+
] : []);
|
|
49
25
|
</script>
|
|
50
26
|
|
|
51
27
|
<template>
|
|
52
|
-
<div
|
|
53
|
-
v-if="event"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
:title="tr(event.name)"
|
|
58
|
-
:breadcrumb="[{ label: t('admin.title', 'Events'), to: links.admin() }, { label: tr(event.name) }]"
|
|
59
|
-
>
|
|
60
|
-
<template #badge>
|
|
61
|
-
<UBadge
|
|
62
|
-
variant="subtle"
|
|
63
|
-
:color="statusColor(event.status)"
|
|
64
|
-
:label="t(`admin.statuses.${event.status}`, event.status)"
|
|
65
|
-
/>
|
|
66
|
-
</template>
|
|
67
|
-
<template #actions>
|
|
68
|
-
<UButton
|
|
69
|
-
v-if="event.status === 'published' && hasPublicPage"
|
|
70
|
-
:to="links.event(event.slug)"
|
|
71
|
-
variant="outline"
|
|
72
|
-
color="neutral"
|
|
73
|
-
icon="i-lucide-external-link"
|
|
74
|
-
:label="t('admin.viewPublic', 'View public page')"
|
|
75
|
-
/>
|
|
76
|
-
<RowMenu
|
|
77
|
-
variant="outline"
|
|
78
|
-
:subject="tr(event.name)"
|
|
79
|
-
:items="actions.menu(event, { open: false, viewPublic: false, onRemoved: () => navigateTo(links.admin()) })"
|
|
80
|
-
/>
|
|
81
|
-
</template>
|
|
82
|
-
</PageHeading>
|
|
83
|
-
|
|
84
|
-
<SectionCard :title="t('admin.keyFacts', 'Event')">
|
|
85
|
-
<form
|
|
86
|
-
id="admin-event-facts"
|
|
87
|
-
class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"
|
|
88
|
-
@submit.prevent="saveFacts"
|
|
28
|
+
<div class="flex flex-col gap-4">
|
|
29
|
+
<template v-if="event">
|
|
30
|
+
<PageHeading
|
|
31
|
+
:title="tr(event.name)"
|
|
32
|
+
:breadcrumb="[{ label: t('admin.title', 'Events'), to: links.admin() }, { label: tr(event.name) }]"
|
|
89
33
|
>
|
|
90
|
-
<
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
:
|
|
94
|
-
class="w-full"
|
|
34
|
+
<template #badge>
|
|
35
|
+
<StatusBadge
|
|
36
|
+
:status="event.status"
|
|
37
|
+
:label="t(`admin.statuses.${event.status}`, event.status)"
|
|
95
38
|
/>
|
|
96
|
-
</
|
|
97
|
-
<
|
|
98
|
-
<UInput
|
|
99
|
-
v-model="facts.startsAt"
|
|
100
|
-
type="datetime-local"
|
|
101
|
-
class="w-full"
|
|
102
|
-
required
|
|
103
|
-
/>
|
|
104
|
-
</UFormField>
|
|
105
|
-
<UFormField :label="t('admin.ends', 'Ends')">
|
|
106
|
-
<UInput
|
|
107
|
-
v-model="facts.endsAt"
|
|
108
|
-
type="datetime-local"
|
|
109
|
-
class="w-full"
|
|
110
|
-
/>
|
|
111
|
-
</UFormField>
|
|
112
|
-
<UFormField :label="t('admin.location', 'Location')">
|
|
113
|
-
<TranslatedInput v-model="facts.location" />
|
|
114
|
-
</UFormField>
|
|
115
|
-
<UFormField :label="t('admin.slug', 'URL name')">
|
|
116
|
-
<UInput
|
|
117
|
-
v-model="facts.slug"
|
|
118
|
-
class="w-full"
|
|
119
|
-
required
|
|
120
|
-
/>
|
|
121
|
-
</UFormField>
|
|
122
|
-
</form>
|
|
123
|
-
<template #footer>
|
|
124
|
-
<div class="flex justify-end">
|
|
39
|
+
</template>
|
|
40
|
+
<template #actions>
|
|
125
41
|
<UButton
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
42
|
+
v-if="event.status === 'published' && hasPublicPage"
|
|
43
|
+
:to="links.event(event.slug)"
|
|
44
|
+
variant="outline"
|
|
45
|
+
color="neutral"
|
|
46
|
+
icon="i-lucide-external-link"
|
|
47
|
+
:label="t('admin.viewPublic', 'View public page')"
|
|
130
48
|
/>
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
49
|
+
<RowMenu
|
|
50
|
+
variant="outline"
|
|
51
|
+
:subject="tr(event.name)"
|
|
52
|
+
:items="actions.menu(event, { open: false, viewPublic: false, onRemoved: () => navigateTo(links.admin()) })"
|
|
53
|
+
/>
|
|
54
|
+
</template>
|
|
55
|
+
</PageHeading>
|
|
134
56
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
:event="event"
|
|
145
|
-
@refresh="refresh"
|
|
146
|
-
/>
|
|
147
|
-
<TicketsTab
|
|
148
|
-
:event="event"
|
|
149
|
-
@refresh="refresh"
|
|
150
|
-
/>
|
|
151
|
-
<AttendeesTab
|
|
152
|
-
:event="event"
|
|
57
|
+
<SectionCard :title="t('admin.keyFacts', 'Event')">
|
|
58
|
+
<DescriptionList :items="facts" />
|
|
59
|
+
</SectionCard>
|
|
60
|
+
|
|
61
|
+
<component
|
|
62
|
+
:is="tabs"
|
|
63
|
+
v-if="tabs"
|
|
64
|
+
name="admin.events"
|
|
65
|
+
:context="{ event }"
|
|
153
66
|
@refresh="refresh"
|
|
154
67
|
/>
|
|
68
|
+
<template v-else>
|
|
69
|
+
<DetailsTab
|
|
70
|
+
:event="event"
|
|
71
|
+
@refresh="refresh"
|
|
72
|
+
/>
|
|
73
|
+
<TicketsTab
|
|
74
|
+
:event="event"
|
|
75
|
+
@refresh="refresh"
|
|
76
|
+
/>
|
|
77
|
+
<AttendeesTab
|
|
78
|
+
:event="event"
|
|
79
|
+
@refresh="refresh"
|
|
80
|
+
/>
|
|
81
|
+
</template>
|
|
155
82
|
</template>
|
|
83
|
+
|
|
84
|
+
<UEmpty
|
|
85
|
+
v-else-if="notFound || !error"
|
|
86
|
+
icon="i-lucide-calendar-x"
|
|
87
|
+
:title="t('notFound', 'Event not found')"
|
|
88
|
+
:actions="[{ label: t('admin.allEvents', 'All events'), to: links.admin(), color: 'neutral', variant: 'outline' }]"
|
|
89
|
+
/>
|
|
90
|
+
<UEmpty
|
|
91
|
+
v-else
|
|
92
|
+
icon="i-lucide-circle-alert"
|
|
93
|
+
:title="t('admin.eventLoadFailed', 'The event could not be loaded')"
|
|
94
|
+
:description="errorMessage(error, '')"
|
|
95
|
+
:actions="[{ label: t('tryAgain', 'Try again'), icon: 'i-lucide-rotate-ccw', color: 'neutral', variant: 'outline', onClick: () => refresh() }]"
|
|
96
|
+
/>
|
|
156
97
|
</div>
|
|
157
|
-
<UEmpty
|
|
158
|
-
v-else
|
|
159
|
-
icon="i-lucide-calendar-x"
|
|
160
|
-
:title="t('notFound', 'Event not found')"
|
|
161
|
-
:actions="[{ label: t('admin.allEvents', 'All events'), to: links.admin(), color: 'neutral', variant: 'outline' }]"
|
|
162
|
-
/>
|
|
163
98
|
</template>
|
|
@@ -1,26 +1,41 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { EVENT_STATUSES } from "../utils/admin-event";
|
|
3
|
+
import { eventsApi } from "../../utils/api";
|
|
2
4
|
defineOptions({ name: "EventsAdminPage" });
|
|
3
5
|
const t = useModuleT("events");
|
|
4
6
|
const tr = useEventsTranslate();
|
|
5
7
|
const format = useFormat();
|
|
6
8
|
const links = useEventLinks();
|
|
7
9
|
const actions = useEventActions();
|
|
8
|
-
const {
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
const { page, pageSize, search, sort: sortState, filters, query, filtered, reset } = useTableQuery({
|
|
11
|
+
sort: { id: "startsAt", desc: true },
|
|
12
|
+
filters: { status: "all" }
|
|
13
|
+
});
|
|
14
|
+
const { sorting, header } = useTableSort(null, sortState);
|
|
15
|
+
const { data, status, error, refresh } = await useFetch(eventsApi.admin.events, { query });
|
|
16
|
+
const rows = computed(() => data.value?.items ?? []);
|
|
17
|
+
const pending = computed(() => status.value === "pending");
|
|
18
|
+
const loading = computed(() => pending.value && !data.value);
|
|
19
|
+
const refreshing = computed(() => pending.value && !!data.value);
|
|
20
|
+
const forbidden = computed(() => error.value?.statusCode === 401 || error.value?.statusCode === 403);
|
|
21
|
+
const statusOptions = computed(() => [
|
|
22
|
+
{ label: t("admin.allStatuses", "All statuses"), value: "all" },
|
|
23
|
+
...EVENT_STATUSES.map((value) => ({ label: t(`admin.statuses.${value}`, value.charAt(0).toUpperCase() + value.slice(1)), value }))
|
|
24
|
+
]);
|
|
25
|
+
const StatusBadge = resolveComponent("StatusBadge");
|
|
11
26
|
const RowMenu = resolveComponent("RowMenu");
|
|
12
27
|
const TableIdentity = resolveComponent("TableIdentity");
|
|
13
28
|
const columns = computed(() => [
|
|
14
29
|
{
|
|
15
30
|
id: "name",
|
|
16
31
|
accessorFn: (row) => tr(row.name),
|
|
17
|
-
header:
|
|
32
|
+
header: t("admin.name", "Name"),
|
|
18
33
|
cell: ({ row }) => h(TableIdentity, { title: tr(row.original.name), description: tr(row.original.location) })
|
|
19
34
|
},
|
|
20
35
|
{
|
|
21
36
|
accessorKey: "status",
|
|
22
37
|
header: header("status", t("admin.status", "Status")),
|
|
23
|
-
cell: ({ row }) => h(
|
|
38
|
+
cell: ({ row }) => h(StatusBadge, { status: row.original.status, label: t(`admin.statuses.${row.original.status}`, row.original.status) })
|
|
24
39
|
},
|
|
25
40
|
{
|
|
26
41
|
accessorKey: "startsAt",
|
|
@@ -33,10 +48,6 @@ const columns = computed(() => [
|
|
|
33
48
|
cell: ({ row }) => h(RowMenu, { items: actions.menu(row.original, { onRemoved: refresh }), subject: tr(row.original.name) })
|
|
34
49
|
}
|
|
35
50
|
]);
|
|
36
|
-
async function create() {
|
|
37
|
-
const created = await actions.create();
|
|
38
|
-
if (created) await navigateTo(links.adminEvent(created.id));
|
|
39
|
-
}
|
|
40
51
|
</script>
|
|
41
52
|
|
|
42
53
|
<template>
|
|
@@ -49,32 +60,83 @@ async function create() {
|
|
|
49
60
|
<UButton
|
|
50
61
|
icon="i-lucide-plus"
|
|
51
62
|
:label="t('admin.create', 'New event')"
|
|
52
|
-
@click="create"
|
|
63
|
+
@click="actions.create()"
|
|
53
64
|
/>
|
|
54
65
|
</template>
|
|
55
66
|
</PageHeading>
|
|
56
67
|
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
<UEmpty
|
|
69
|
+
v-if="error && !data"
|
|
70
|
+
:icon="forbidden ? 'i-lucide-lock' : 'i-lucide-circle-alert'"
|
|
71
|
+
:title="forbidden ? t('admin.forbidden', 'You do not have access to events') : t('admin.loadFailed', 'Events could not be loaded')"
|
|
72
|
+
:description="errorMessage(error, '')"
|
|
73
|
+
:actions="forbidden ? [] : [{ label: t('tryAgain', 'Try again'), icon: 'i-lucide-rotate-ccw', color: 'neutral', variant: 'outline', onClick: () => refresh() }]"
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
<template v-else>
|
|
77
|
+
<PageToolbar>
|
|
78
|
+
<UInput
|
|
79
|
+
v-model="search"
|
|
80
|
+
icon="i-lucide-search"
|
|
81
|
+
:loading="pending"
|
|
82
|
+
:placeholder="t('admin.search', 'Search events')"
|
|
83
|
+
:aria-label="t('admin.search', 'Search events')"
|
|
84
|
+
class="w-full sm:max-w-xs"
|
|
85
|
+
/>
|
|
86
|
+
<USelect
|
|
87
|
+
v-model="filters.status"
|
|
88
|
+
:items="statusOptions"
|
|
89
|
+
:aria-label="t('admin.status', 'Status')"
|
|
90
|
+
class="w-44"
|
|
91
|
+
/>
|
|
92
|
+
<UButton
|
|
93
|
+
v-if="filtered"
|
|
94
|
+
variant="ghost"
|
|
95
|
+
color="neutral"
|
|
96
|
+
icon="i-lucide-x"
|
|
97
|
+
:label="t('admin.clearFilters', 'Clear filters')"
|
|
98
|
+
@click="reset"
|
|
99
|
+
/>
|
|
100
|
+
</PageToolbar>
|
|
101
|
+
|
|
102
|
+
<TableCard
|
|
103
|
+
:loading="loading"
|
|
104
|
+
:refreshing="refreshing"
|
|
67
105
|
>
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
106
|
+
<UTable
|
|
107
|
+
v-model:sorting="sorting"
|
|
108
|
+
:data="rows"
|
|
109
|
+
:columns="columns"
|
|
110
|
+
:sorting-options="{ manualSorting: true }"
|
|
111
|
+
:ui="tableUi()"
|
|
112
|
+
@select="(_, row) => navigateTo(links.adminEvent(row.original.id))"
|
|
113
|
+
>
|
|
114
|
+
<template #empty>
|
|
115
|
+
<UEmpty
|
|
116
|
+
v-if="filtered"
|
|
117
|
+
variant="naked"
|
|
118
|
+
icon="i-lucide-search"
|
|
119
|
+
:title="t('admin.noMatches', 'No events match your search')"
|
|
120
|
+
:actions="[{ label: t('admin.clearFilters', 'Clear filters'), color: 'neutral', variant: 'outline', onClick: reset }]"
|
|
121
|
+
/>
|
|
122
|
+
<UEmpty
|
|
123
|
+
v-else
|
|
124
|
+
variant="naked"
|
|
125
|
+
icon="i-lucide-calendar"
|
|
126
|
+
:title="t('admin.empty', 'No events yet')"
|
|
127
|
+
:description="t('admin.emptyDescription', 'Create the first event to start selling tickets.')"
|
|
128
|
+
:actions="[{ label: t('admin.create', 'New event'), icon: 'i-lucide-plus', onClick: () => actions.create() }]"
|
|
129
|
+
/>
|
|
130
|
+
</template>
|
|
131
|
+
</UTable>
|
|
132
|
+
<template #footer>
|
|
133
|
+
<TablePagination
|
|
134
|
+
v-model:page="page"
|
|
135
|
+
v-model:page-size="pageSize"
|
|
136
|
+
:total="data?.total ?? 0"
|
|
75
137
|
/>
|
|
76
138
|
</template>
|
|
77
|
-
</
|
|
78
|
-
</
|
|
139
|
+
</TableCard>
|
|
140
|
+
</template>
|
|
79
141
|
</div>
|
|
80
142
|
</template>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { eventsApi } from "../../utils/api";
|
|
2
3
|
const hasMdc = typeof resolveComponent("MDC") !== "string";
|
|
3
4
|
const t = useModuleT("events");
|
|
4
5
|
const route = useRoute();
|
|
@@ -24,7 +25,7 @@ onMounted(async () => {
|
|
|
24
25
|
if (payment.value !== "success" || !returnedTicket.value) return;
|
|
25
26
|
paidState.value = "waiting";
|
|
26
27
|
for (let attempt = 0; attempt < 20; attempt++) {
|
|
27
|
-
const ticket = await $fetch(
|
|
28
|
+
const ticket = await $fetch(eventsApi.me.ticket(returnedTicket.value)).catch(() => null);
|
|
28
29
|
if (ticket?.status === "paid") {
|
|
29
30
|
paidState.value = "paid";
|
|
30
31
|
await refresh();
|
|
@@ -115,7 +116,10 @@ defineOptions({ name: "EventDetailPage" });
|
|
|
115
116
|
</div>
|
|
116
117
|
|
|
117
118
|
<div class="grid gap-3 sm:grid-cols-2">
|
|
118
|
-
<div
|
|
119
|
+
<div
|
|
120
|
+
class="flex items-start gap-3 p-4"
|
|
121
|
+
:class="surfaceClass"
|
|
122
|
+
>
|
|
119
123
|
<UIcon
|
|
120
124
|
name="i-lucide-calendar"
|
|
121
125
|
class="text-primary mt-0.5 size-5 shrink-0"
|
|
@@ -135,8 +139,8 @@ defineOptions({ name: "EventDetailPage" });
|
|
|
135
139
|
:href="event.locationUrl || void 0"
|
|
136
140
|
:target="event.locationUrl ? '_blank' : void 0"
|
|
137
141
|
:rel="event.locationUrl ? 'noopener' : void 0"
|
|
138
|
-
class="
|
|
139
|
-
:class="event.locationUrl ? 'hover:bg-elevated transition' : ''"
|
|
142
|
+
class="flex items-start gap-3 p-4"
|
|
143
|
+
:class="[surfaceClass, event.locationUrl ? 'hover:bg-elevated transition' : '']"
|
|
140
144
|
>
|
|
141
145
|
<UIcon
|
|
142
146
|
name="i-lucide-map-pin"
|
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
defineOptions({ name: "EventTicketsPage" });
|
|
3
3
|
const t = useModuleT("events");
|
|
4
|
-
const { tickets, pay } = useMyTickets();
|
|
4
|
+
const { tickets, loading, error, refresh, pay } = await useMyTickets();
|
|
5
5
|
const links = useEventLinks();
|
|
6
6
|
</script>
|
|
7
7
|
|
|
8
8
|
<template>
|
|
9
9
|
<div class="flex flex-col gap-4">
|
|
10
|
+
<PageHeading
|
|
11
|
+
:title="t('menu.tickets.label', 'Tickets')"
|
|
12
|
+
:description="t('menu.tickets.description', 'Your event tickets.')"
|
|
13
|
+
/>
|
|
14
|
+
<div
|
|
15
|
+
v-if="loading"
|
|
16
|
+
class="grid gap-4 md:grid-cols-2"
|
|
17
|
+
aria-busy="true"
|
|
18
|
+
>
|
|
19
|
+
<SectionCard
|
|
20
|
+
v-for="index in 2"
|
|
21
|
+
:key="index"
|
|
22
|
+
loading
|
|
23
|
+
/>
|
|
24
|
+
</div>
|
|
25
|
+
<UEmpty
|
|
26
|
+
v-else-if="error && !tickets.length"
|
|
27
|
+
icon="i-lucide-circle-alert"
|
|
28
|
+
:title="t('ticketsLoadFailed', 'Your tickets could not be loaded')"
|
|
29
|
+
:description="errorMessage(error, '')"
|
|
30
|
+
:actions="[{ label: t('tryAgain', 'Try again'), icon: 'i-lucide-rotate-ccw', color: 'neutral', variant: 'outline', onClick: () => refresh() }]"
|
|
31
|
+
/>
|
|
10
32
|
<UEmpty
|
|
11
|
-
v-if="!tickets.length"
|
|
33
|
+
v-else-if="!tickets.length"
|
|
12
34
|
icon="i-lucide-ticket"
|
|
13
35
|
:title="t('noTicketsTitle', 'No tickets yet')"
|
|
14
36
|
:description="t('noTickets', 'You do not have any tickets yet.')"
|