@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.
Files changed (97) hide show
  1. package/dist/module.d.mts +105 -1
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +40 -31
  4. package/dist/runtime/app/components/EventAddonGroupSlideover.d.vue.ts +18 -0
  5. package/dist/runtime/app/components/EventAddonGroupSlideover.vue +66 -0
  6. package/dist/runtime/app/components/EventAddonGroupSlideover.vue.d.ts +18 -0
  7. package/dist/runtime/app/components/EventAddonSlideover.d.vue.ts +20 -0
  8. package/dist/runtime/app/components/EventAddonSlideover.vue +103 -0
  9. package/dist/runtime/app/components/EventAddonSlideover.vue.d.ts +20 -0
  10. package/dist/runtime/app/components/EventAdminAddonGroups.d.vue.ts +1 -9
  11. package/dist/runtime/app/components/EventAdminAddonGroups.vue +6 -48
  12. package/dist/runtime/app/components/EventAdminAddonGroups.vue.d.ts +1 -9
  13. package/dist/runtime/app/components/EventFormSlideover.vue +45 -23
  14. package/dist/runtime/app/components/EventTicketCard.vue +59 -62
  15. package/dist/runtime/app/components/EventTicketTypeSlideover.d.vue.ts +20 -0
  16. package/dist/runtime/app/components/EventTicketTypeSlideover.vue +176 -0
  17. package/dist/runtime/app/components/EventTicketTypeSlideover.vue.d.ts +20 -0
  18. package/dist/runtime/app/composables/index.d.ts +2 -0
  19. package/dist/runtime/app/composables/index.js +1 -0
  20. package/dist/runtime/app/composables/useEventActions.d.ts +3 -3
  21. package/dist/runtime/app/composables/useEventActions.js +9 -4
  22. package/dist/runtime/app/composables/useEventCatalogActions.d.ts +33 -0
  23. package/dist/runtime/app/composables/useEventCatalogActions.js +107 -0
  24. package/dist/runtime/app/composables/useEvents.d.ts +7 -6
  25. package/dist/runtime/app/composables/useEvents.js +17 -10
  26. package/dist/runtime/app/pages/admin-event.vue +71 -136
  27. package/dist/runtime/app/pages/admin.vue +91 -29
  28. package/dist/runtime/app/pages/event.vue +8 -4
  29. package/dist/runtime/app/pages/tickets.vue +24 -2
  30. package/dist/runtime/app/tabs/Attendees.vue +78 -39
  31. package/dist/runtime/app/tabs/Details.d.vue.ts +1 -1
  32. package/dist/runtime/app/tabs/Details.vue +112 -30
  33. package/dist/runtime/app/tabs/Details.vue.d.ts +1 -1
  34. package/dist/runtime/app/tabs/Tickets.vue +105 -132
  35. package/dist/runtime/app/utils/admin-event.d.ts +53 -38
  36. package/dist/runtime/app/utils/admin-event.js +14 -0
  37. package/dist/runtime/app/widgets/UpcomingEvents.vue +2 -1
  38. package/dist/runtime/app/widgets/UpcomingEventsStat.vue +2 -1
  39. package/dist/runtime/i18n/lang/da.json +24 -3
  40. package/dist/runtime/i18n/lang/en.json +24 -3
  41. package/dist/runtime/server/bootstrap/my-tickets.d.ts +7 -0
  42. package/dist/runtime/server/{routes/public/tickets-me.get.js → bootstrap/my-tickets.js} +2 -2
  43. package/dist/runtime/server/routes/admin/addon-group.delete.js +6 -3
  44. package/dist/runtime/server/routes/admin/addon-group.patch.js +6 -4
  45. package/dist/runtime/server/routes/admin/addon-groups.post.js +8 -6
  46. package/dist/runtime/server/routes/admin/addon.delete.js +6 -3
  47. package/dist/runtime/server/routes/admin/addon.patch.js +8 -5
  48. package/dist/runtime/server/routes/admin/addons.post.js +8 -6
  49. package/dist/runtime/server/routes/admin/attendees.get.js +38 -8
  50. package/dist/runtime/server/routes/{public → admin}/check-in.post.js +4 -3
  51. package/dist/runtime/server/routes/admin/event-clone.post.js +37 -19
  52. package/dist/runtime/server/routes/admin/event.delete.js +7 -4
  53. package/dist/runtime/server/routes/admin/event.get.js +4 -2
  54. package/dist/runtime/server/routes/admin/event.patch.js +4 -2
  55. package/dist/runtime/server/routes/admin/events.get.js +36 -3
  56. package/dist/runtime/server/routes/admin/events.post.js +3 -4
  57. package/dist/runtime/server/routes/admin/membership-options.get.js +3 -2
  58. package/dist/runtime/server/routes/admin/ticket-cancel.post.js +7 -8
  59. package/dist/runtime/server/routes/admin/ticket-refund.post.js +7 -5
  60. package/dist/runtime/server/routes/admin/ticket-type.delete.js +7 -4
  61. package/dist/runtime/server/routes/admin/ticket-type.patch.js +4 -2
  62. package/dist/runtime/server/routes/admin/ticket-types.post.js +10 -7
  63. package/dist/runtime/server/routes/admin/upcoming.get.js +7 -3
  64. package/dist/runtime/server/routes/me/ticket-pay.post.js +13 -0
  65. package/dist/runtime/server/routes/me/ticket.get.js +6 -0
  66. package/dist/runtime/server/routes/me/tickets.get.js +2 -0
  67. package/dist/runtime/server/routes/public/event.get.js +5 -2
  68. package/dist/runtime/server/routes/public/purchase.post.js +3 -1
  69. package/dist/runtime/server/utils/admin-dto.d.ts +30 -0
  70. package/dist/runtime/server/utils/admin-dto.js +101 -0
  71. package/dist/runtime/server/utils/admin-schemas.js +2 -1
  72. package/dist/runtime/server/utils/admin.d.ts +3 -1
  73. package/dist/runtime/server/utils/admin.js +20 -6
  74. package/dist/runtime/server/utils/auth.d.ts +5 -4
  75. package/dist/runtime/server/utils/auth.js +6 -7
  76. package/dist/runtime/server/utils/tickets.js +6 -11
  77. package/dist/runtime/utils/api.d.ts +48 -0
  78. package/dist/runtime/utils/api.js +51 -0
  79. package/dist/runtime/utils/index.d.ts +2 -1
  80. package/dist/runtime/utils/index.js +1 -0
  81. package/dist/types.d.mts +1 -1
  82. package/package.json +21 -16
  83. package/dist/runtime/app/components/EventAdminAddonDialog.d.vue.ts +0 -11
  84. package/dist/runtime/app/components/EventAdminAddonDialog.vue +0 -112
  85. package/dist/runtime/app/components/EventAdminAddonDialog.vue.d.ts +0 -11
  86. package/dist/runtime/app/components/EventAdminAddonGroupDialog.d.vue.ts +0 -11
  87. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue +0 -78
  88. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue.d.ts +0 -11
  89. package/dist/runtime/app/components/EventAdminTicketTypeDialog.d.vue.ts +0 -20
  90. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue +0 -174
  91. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue.d.ts +0 -20
  92. package/dist/runtime/server/routes/public/ticket-pay.post.js +0 -10
  93. package/dist/runtime/server/routes/public/ticket.get.js +0 -4
  94. /package/dist/runtime/server/routes/{public → admin}/check-in.post.d.ts +0 -0
  95. /package/dist/runtime/server/routes/{public → me}/ticket-pay.post.d.ts +0 -0
  96. /package/dist/runtime/server/routes/{public → me}/ticket.get.d.ts +0 -0
  97. /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
- export declare function useMyTickets(): {
20
- tickets: any;
21
- status: any;
22
- refresh: any;
23
- pay: (ticketId: string) => Promise<void>;
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("/api/events", { key: "events:list", default: () => [] });
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(() => `/api/events/${toValue(slug)}`, {
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(`/api/events/${toValue(slug)}/purchase`, {
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 { data, status, refresh } = useFetch("/api/events/tickets/me", {
38
+ const fetched = useCachedFetch(eventsApi.me.tickets, {
38
39
  key: "events:tickets:mine",
39
- immediate: loggedIn.value,
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(`/api/events/tickets/${ticketId}/pay`, { method: "POST" });
50
+ const { url } = await $fetch(eventsApi.me.pay(ticketId), { method: "POST" });
45
51
  await navigateTo(url, { external: true });
46
52
  }
47
- return { tickets: data, status, refresh, pay };
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 { toLocalInput } from "../utils/admin-event";
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 { busy, run } = useAction();
15
- const { data: event, refresh } = await useFetch(() => `/api/events/admin/events/${id.value}`);
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 = reactive({
19
- status: "draft",
20
- startsAt: "",
21
- endsAt: "",
22
- location: void 0,
23
- slug: ""
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
- class="flex flex-col gap-4"
55
- >
56
- <PageHeading
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
- <UFormField :label="t('admin.status', 'Status')">
91
- <USelect
92
- v-model="facts.status"
93
- :items="statuses"
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
- </UFormField>
97
- <UFormField :label="t('admin.starts', 'Starts')">
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
- type="submit"
127
- form="admin-event-facts"
128
- :loading="busy"
129
- :label="t('admin.saveEvent', 'Save event')"
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
- </div>
132
- </template>
133
- </SectionCard>
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
- <component
136
- :is="tabs"
137
- v-if="tabs"
138
- name="admin.events"
139
- :context="{ event }"
140
- @refresh="refresh"
141
- />
142
- <template v-else>
143
- <DetailsTab
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 { data: events, status, refresh } = await useFetch("/api/events/admin/events", { default: () => [] });
9
- const { sorting, header } = useTableSort({ id: "startsAt", desc: true });
10
- const UBadge = resolveComponent("UBadge");
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: header("name", t("admin.name", "Name")),
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(UBadge, { variant: "subtle", color: statusColor(row.original.status), label: t(`admin.statuses.${row.original.status}`, row.original.status) })
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
- <TableCard
58
- :loading="status === 'pending' && !events.length"
59
- :refreshing="status === 'pending' && !!events.length"
60
- >
61
- <UTable
62
- v-model:sorting="sorting"
63
- :data="events"
64
- :columns="columns"
65
- :ui="tableUi()"
66
- @select="(_, row) => navigateTo(links.adminEvent(row.original.id))"
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
- <template #empty>
69
- <UEmpty
70
- variant="naked"
71
- icon="i-lucide-calendar"
72
- :title="t('admin.empty', 'No events yet')"
73
- :description="t('admin.emptyDescription', 'Create the first event to start selling tickets.')"
74
- :actions="[{ label: t('admin.create', 'New event'), icon: 'i-lucide-plus', onClick: create }]"
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
- </UTable>
78
- </TableCard>
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(`/api/events/tickets/${returnedTicket.value}`).catch(() => null);
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 class="bg-elevated/50 ring-default flex items-start gap-3 rounded-lg p-4 ring">
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="bg-elevated/50 ring-default flex items-start gap-3 rounded-lg p-4 ring"
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.')"