@frigear-nu/events 0.0.10 → 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.
Files changed (56) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +6 -4
  3. package/dist/runtime/app/components/EventAdminAddonDialog.d.vue.ts +2 -2
  4. package/dist/runtime/app/components/EventAdminAddonDialog.vue +3 -4
  5. package/dist/runtime/app/components/EventAdminAddonDialog.vue.d.ts +2 -2
  6. package/dist/runtime/app/components/EventAdminAddonGroupDialog.d.vue.ts +2 -2
  7. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue +3 -4
  8. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue.d.ts +2 -2
  9. package/dist/runtime/app/components/EventAdminAddonGroups.d.vue.ts +8 -8
  10. package/dist/runtime/app/components/EventAdminAddonGroups.vue +46 -55
  11. package/dist/runtime/app/components/EventAdminAddonGroups.vue.d.ts +8 -8
  12. package/dist/runtime/app/components/EventAdminTicketTypeDialog.d.vue.ts +2 -2
  13. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue +3 -4
  14. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue.d.ts +2 -2
  15. package/dist/runtime/app/components/EventCard.vue +7 -12
  16. package/dist/runtime/app/components/EventFormSlideover.d.vue.ts +13 -0
  17. package/dist/runtime/app/components/EventFormSlideover.vue +70 -0
  18. package/dist/runtime/app/components/EventFormSlideover.vue.d.ts +13 -0
  19. package/dist/runtime/app/components/EventTicketCard.vue +5 -7
  20. package/dist/runtime/app/components/EventTicketPicker.vue +5 -6
  21. package/dist/runtime/app/composables/index.d.ts +2 -0
  22. package/dist/runtime/app/composables/index.js +2 -0
  23. package/dist/runtime/app/composables/useEventActions.d.ts +26 -0
  24. package/dist/runtime/app/composables/useEventActions.js +61 -0
  25. package/dist/runtime/app/composables/useEventLinks.d.ts +12 -0
  26. package/dist/runtime/app/composables/useEventLinks.js +5 -0
  27. package/dist/runtime/app/composables/useEvents.d.ts +5 -2
  28. package/dist/runtime/app/pages/admin-event.vue +33 -21
  29. package/dist/runtime/app/pages/admin.vue +61 -135
  30. package/dist/runtime/app/pages/event.d.vue.ts +2 -2
  31. package/dist/runtime/app/pages/event.vue +113 -152
  32. package/dist/runtime/app/pages/event.vue.d.ts +2 -2
  33. package/dist/runtime/app/pages/events.vue +33 -31
  34. package/dist/runtime/app/pages/tickets.vue +21 -28
  35. package/dist/runtime/app/tabs/Attendees.d.vue.ts +2 -2
  36. package/dist/runtime/app/tabs/Attendees.vue +64 -60
  37. package/dist/runtime/app/tabs/Attendees.vue.d.ts +2 -2
  38. package/dist/runtime/app/tabs/Details.d.vue.ts +2 -2
  39. package/dist/runtime/app/tabs/Details.vue +4 -5
  40. package/dist/runtime/app/tabs/Details.vue.d.ts +2 -2
  41. package/dist/runtime/app/tabs/Tickets.d.vue.ts +2 -2
  42. package/dist/runtime/app/tabs/Tickets.vue +92 -99
  43. package/dist/runtime/app/tabs/Tickets.vue.d.ts +2 -2
  44. package/dist/runtime/app/utils/admin-event.d.ts +9 -12
  45. package/dist/runtime/app/utils/admin-event.js +0 -17
  46. package/dist/runtime/app/widgets/UpcomingEvents.vue +7 -9
  47. package/dist/runtime/app/widgets/UpcomingEventsStat.vue +5 -6
  48. package/dist/runtime/i18n/lang/da.json +111 -3
  49. package/dist/runtime/i18n/lang/en.json +111 -3
  50. package/dist/runtime/menus.js +2 -4
  51. package/dist/runtime/server/utils/admin.d.ts +5 -5
  52. package/dist/runtime/utils/index.d.ts +1 -0
  53. package/dist/runtime/utils/index.js +1 -0
  54. package/dist/runtime/utils/links.d.ts +21 -0
  55. package/dist/runtime/utils/links.js +12 -0
  56. package/package.json +21 -21
@@ -1,6 +1,5 @@
1
1
  <script setup>
2
2
  import { withMdcData } from "@frigear-nu/ui/markdown";
3
- import { formatMoney as formatPrice } from "@frigear-nu/ui/money";
4
3
  import { DEFAULT_CURRENCY, ticketMdcData } from "../../utils";
5
4
  const props = defineProps({
6
5
  ticketTypes: { type: Array, required: true },
@@ -9,7 +8,7 @@ const props = defineProps({
9
8
  });
10
9
  const emit = defineEmits(["purchase"]);
11
10
  const t = useModuleT("events");
12
- const locale = useEventsLocale();
11
+ const format = useFormat();
13
12
  const hasMdc = typeof resolveComponent("MDC") !== "string";
14
13
  const tr = useEventsTranslate();
15
14
  const selectedId = ref();
@@ -83,7 +82,7 @@ function toggle(groupId, addonId, multiple) {
83
82
  <span
84
83
  v-if="!type.hidePrice"
85
84
  class="ml-auto font-bold text-primary"
86
- >{{ type.price === 0 ? t("picker.free", "Free") : formatPrice(type.price, type.currency, locale) }}</span>
85
+ >{{ type.price === 0 ? t("picker.free", "Free") : format.money(type.price, type.currency) }}</span>
87
86
  </div>
88
87
  <div
89
88
  v-if="type.description"
@@ -91,7 +90,7 @@ function toggle(groupId, addonId, multiple) {
91
90
  >
92
91
  <MDC
93
92
  v-if="hasMdc"
94
- :value="withMdcData(tr(type.description), ticketMdcData(type, tr(type.name), locale))"
93
+ :value="withMdcData(tr(type.description), ticketMdcData(type, tr(type.name), format.locale.value))"
95
94
  class="prose prose-sm max-w-none text-muted"
96
95
  />
97
96
  <template v-else>
@@ -128,7 +127,7 @@ function toggle(groupId, addonId, multiple) {
128
127
  >
129
128
  {{ t("picker.soldOut", "Sold out") }}
130
129
  </UBadge>
131
- <span class="ml-auto text-sm">{{ addon.price === 0 ? t("picker.included", "Included") : `+ ${formatPrice(addon.price, selected.currency, locale)}` }}</span>
130
+ <span class="ml-auto text-sm">{{ addon.price === 0 ? t("picker.included", "Included") : `+ ${format.money(addon.price, selected.currency)}` }}</span>
132
131
  </button>
133
132
  </fieldset>
134
133
 
@@ -140,7 +139,7 @@ function toggle(groupId, addonId, multiple) {
140
139
  :disabled="!selected || !isAvailable(selected) || missingRequired"
141
140
  @click="emit('purchase', selected.id, addonIds)"
142
141
  >
143
- {{ total === 0 ? t("picker.getFree", "Get free ticket") : `${t("picker.buy", "Buy ticket")} \xB7 ${formatPrice(total, selected?.currency ?? DEFAULT_CURRENCY, locale)}` }}
142
+ {{ total === 0 ? t("picker.getFree", "Get free ticket") : `${t("picker.buy", "Buy ticket")} \xB7 ${format.money(total, selected?.currency ?? DEFAULT_CURRENCY)}` }}
144
143
  </UButton>
145
144
  <UAlert
146
145
  v-else
@@ -1 +1,3 @@
1
1
  export { useEvents, useEvent, useMyTickets, useEventsLocale, useEventsFallbackLocale, useEventsTranslate } from './useEvents.js';
2
+ export { useEventLinks } from './useEventLinks.js';
3
+ export { useEventActions } from './useEventActions.js';
@@ -1 +1,3 @@
1
1
  export { useEvents, useEvent, useMyTickets, useEventsLocale, useEventsFallbackLocale, useEventsTranslate } from "./useEvents.js";
2
+ export { useEventLinks } from "./useEventLinks.js";
3
+ export { useEventActions } from "./useEventActions.js";
@@ -0,0 +1,26 @@
1
+ import type { DropdownMenuItem } from '@nuxt/ui';
2
+ import type { StaffEventSummary } from '../utils/admin-event.js';
3
+ export type EventMenuOptions = {
4
+ /** Items for the middle group, between the edits and the delete action. */
5
+ extra?: DropdownMenuItem[];
6
+ /** Runs after the event was deleted (refresh a list, leave the detail page). */
7
+ onRemoved?: () => unknown;
8
+ /** Leave out "Open", e.g. on the event's own page. Defaults to `true`. */
9
+ open?: boolean;
10
+ /** Leave out "View public page", e.g. when the page shows its own button. Defaults to `true`. */
11
+ viewPublic?: boolean;
12
+ };
13
+ /**
14
+ * What staff can do with an event, shared by the list and the event page: `create` (a slideover that resolves with the
15
+ * new event, or `false` when cancelled), `duplicate`, `remove` (confirms first) and `menu(event)`, the row menu in the
16
+ * same order everywhere: Open, View public page, Duplicate | extras | Delete.
17
+ */
18
+ export declare function useEventActions(): {
19
+ create: () => Promise<{
20
+ id: string;
21
+ slug: string;
22
+ } | false | undefined>;
23
+ duplicate: (event: StaffEventSummary) => Promise<any>;
24
+ remove: (event: StaffEventSummary) => Promise<boolean>;
25
+ menu: (event: StaffEventSummary, options?: EventMenuOptions) => DropdownMenuItem[][];
26
+ };
@@ -0,0 +1,61 @@
1
+ import EventFormSlideover from "../components/EventFormSlideover.vue";
2
+ export function useEventActions() {
3
+ const t = useModuleT("events");
4
+ const overlay = useOverlay();
5
+ const confirm = useConfirmDialog();
6
+ const { run } = useAction();
7
+ const links = useEventLinks();
8
+ const tr = useEventsTranslate();
9
+ const hasPublicPage = !!useRuntimeConfig().public.events?.pages?.event;
10
+ async function create() {
11
+ return overlay.create(EventFormSlideover, { destroyOnClose: true }).open();
12
+ }
13
+ async function duplicate(event) {
14
+ const copy = await run(
15
+ () => $fetch(`/api/events/admin/events/${event.id}/clone`, { method: "POST" }),
16
+ { success: t("admin.duplicated", "Event duplicated as a draft") }
17
+ );
18
+ if (copy) await navigateTo(links.adminEvent(copy.id));
19
+ return copy;
20
+ }
21
+ async function remove(event) {
22
+ const name = tr(event.name);
23
+ const confirmed = await confirm({
24
+ title: t("admin.deleteTitle", "Delete {name}?", { name }),
25
+ description: t("admin.deleteDescription", "Its ticket types and add-ons are deleted too. Events with paid tickets cannot be deleted."),
26
+ confirmText: t("admin.deleteAction", "Delete event"),
27
+ destructive: true
28
+ });
29
+ if (!confirmed) return false;
30
+ const done = await run(
31
+ () => $fetch(`/api/events/admin/events/${event.id}`, { method: "DELETE" }),
32
+ { success: t("admin.deleted", "Deleted {name}", { name }), error: t("admin.deleteFailed", "Could not delete the event") }
33
+ );
34
+ return done !== void 0;
35
+ }
36
+ function menu(event, options = {}) {
37
+ const main = [];
38
+ if (options.open !== false) {
39
+ main.push({ label: t("admin.open", "Open"), icon: "i-lucide-arrow-right", to: links.adminEvent(event.id) });
40
+ }
41
+ if (options.viewPublic !== false && hasPublicPage && event.status === "published") {
42
+ main.push({ label: t("admin.viewPublic", "View public page"), icon: "i-lucide-external-link", to: links.event(event.slug), target: "_blank" });
43
+ }
44
+ const edits = [
45
+ { label: t("admin.clone", "Duplicate"), icon: "i-lucide-copy", onSelect: () => {
46
+ void duplicate(event);
47
+ } },
48
+ ...options.extra ?? []
49
+ ];
50
+ const destructive = [{
51
+ label: t("admin.deleteAction", "Delete event"),
52
+ icon: "i-lucide-trash-2",
53
+ color: "error",
54
+ onSelect: async () => {
55
+ if (await remove(event)) await options.onRemoved?.();
56
+ }
57
+ }];
58
+ return [main, edits, destructive].filter((group) => group.length);
59
+ }
60
+ return { create, duplicate, remove, menu };
61
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Locale-aware links to the event pages (`/events`, `/events/:slug`, `/account/tickets`, the admin list and event
3
+ * page). Always use these instead of building the path by hand: they keep the locale prefix (`/en/events` stays under
4
+ * `/en`) and follow `events.settings.pages`.
5
+ */
6
+ export declare function useEventLinks(): {
7
+ events: () => string;
8
+ event: (slug: string) => string;
9
+ tickets: () => string;
10
+ admin: () => string;
11
+ adminEvent: (id: string) => string;
12
+ };
@@ -0,0 +1,5 @@
1
+ import { createEventLinks } from "../../utils/links.js";
2
+ export function useEventLinks() {
3
+ const { localize } = useLocaleLink();
4
+ return createEventLinks(useRuntimeConfig().public.events?.pages, localize);
5
+ }
@@ -1,4 +1,4 @@
1
- import type { Translated } from '../../../types.js';
1
+ import type { TicketDto, Translated } from '../../../types.js';
2
2
  export declare function useEvents(): Promise<{
3
3
  events: any;
4
4
  grouped: any;
@@ -11,7 +11,10 @@ export declare function useEvent(slug: MaybeRefOrGetter<string>): Promise<{
11
11
  status: any;
12
12
  error: any;
13
13
  refresh: any;
14
- purchase: (ticketTypeId: string, addonIds?: string[]) => Promise<any>;
14
+ purchase: (ticketTypeId: string, addonIds?: string[]) => Promise<{
15
+ ticket: TicketDto;
16
+ url?: string;
17
+ }>;
15
18
  }>;
16
19
  export declare function useMyTickets(): {
17
20
  tickets: any;
@@ -2,14 +2,16 @@
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, useAdminAction } from "../utils/admin-event";
5
+ import { toLocalInput } from "../utils/admin-event";
6
6
  defineOptions({ name: "EventsAdminEventPage" });
7
7
  const t = useModuleT("events");
8
8
  const route = useRoute();
9
9
  const tr = useEventsTranslate();
10
- const pages = useRuntimeConfig().public.events?.pages;
10
+ const links = useEventLinks();
11
+ const actions = useEventActions();
12
+ const hasPublicPage = !!useRuntimeConfig().public.events?.pages?.event;
11
13
  const id = computed(() => String(route.params.id));
12
- const { busy, run } = useAdminAction();
14
+ const { busy, run } = useAction();
13
15
  const { data: event, refresh } = await useFetch(() => `/api/events/admin/events/${id.value}`);
14
16
  const Tabs = resolveComponent("WidgetTabs");
15
17
  const tabs = typeof Tabs === "string" ? void 0 : Tabs;
@@ -31,7 +33,6 @@ watch(event, (value) => {
31
33
  });
32
34
  }, { immediate: true });
33
35
  const statuses = computed(() => ["draft", "published", "archived"].map((value) => ({ value, label: t(`admin.statuses.${value}`, value.charAt(0).toUpperCase() + value.slice(1)) })));
34
- const statusColor = { draft: "warning", published: "success", archived: "neutral" };
35
36
  const saveFacts = () => run(async () => {
36
37
  await $fetch(`/api/events/admin/events/${id.value}`, {
37
38
  method: "PATCH",
@@ -44,33 +45,43 @@ const saveFacts = () => run(async () => {
44
45
  }
45
46
  });
46
47
  await refresh();
47
- }, t("admin.saved", "Saved."));
48
+ }, { success: t("admin.saved", "Saved.") });
48
49
  </script>
49
50
 
50
51
  <template>
51
- <AdminPage v-if="event">
52
- <AdminPageHeader
52
+ <div
53
+ v-if="event"
54
+ class="flex flex-col gap-4"
55
+ >
56
+ <PageHeading
53
57
  :title="tr(event.name)"
54
- :back="{ label: t('admin.title', 'Events'), to: pages?.admin || '/admin/events' }"
58
+ :breadcrumb="[{ label: t('admin.title', 'Events'), to: links.admin() }, { label: tr(event.name) }]"
55
59
  >
56
- <UBadge
57
- variant="subtle"
58
- :color="statusColor[event.status] ?? 'neutral'"
59
- :label="t(`admin.statuses.${event.status}`, event.status)"
60
- />
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>
61
67
  <template #actions>
62
68
  <UButton
63
- v-if="event.status === 'published' && pages?.event"
64
- :to="pages.event.replace(':slug', encodeURIComponent(event.slug))"
69
+ v-if="event.status === 'published' && hasPublicPage"
70
+ :to="links.event(event.slug)"
65
71
  variant="outline"
66
72
  color="neutral"
67
73
  icon="i-lucide-external-link"
68
74
  :label="t('admin.viewPublic', 'View public page')"
69
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
+ />
70
81
  </template>
71
- </AdminPageHeader>
82
+ </PageHeading>
72
83
 
73
- <AdminSection :title="t('admin.keyFacts', 'Event')">
84
+ <SectionCard :title="t('admin.keyFacts', 'Event')">
74
85
  <form
75
86
  id="admin-event-facts"
76
87
  class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"
@@ -115,11 +126,11 @@ const saveFacts = () => run(async () => {
115
126
  type="submit"
116
127
  form="admin-event-facts"
117
128
  :loading="busy"
118
- :label="t('admin.save', 'Save')"
129
+ :label="t('admin.saveEvent', 'Save event')"
119
130
  />
120
131
  </div>
121
132
  </template>
122
- </AdminSection>
133
+ </SectionCard>
123
134
 
124
135
  <component
125
136
  :is="tabs"
@@ -142,10 +153,11 @@ const saveFacts = () => run(async () => {
142
153
  @refresh="refresh"
143
154
  />
144
155
  </template>
145
- </AdminPage>
146
- <AdminEmpty
156
+ </div>
157
+ <UEmpty
147
158
  v-else
148
159
  icon="i-lucide-calendar-x"
149
160
  :title="t('notFound', 'Event not found')"
161
+ :actions="[{ label: t('admin.allEvents', 'All events'), to: links.admin(), color: 'neutral', variant: 'outline' }]"
150
162
  />
151
163
  </template>
@@ -1,154 +1,80 @@
1
1
  <script setup>
2
2
  defineOptions({ name: "EventsAdminPage" });
3
3
  const t = useModuleT("events");
4
- const toast = useToast();
5
- const locale = useEventsLocale();
6
4
  const tr = useEventsTranslate();
7
- const pages = useRuntimeConfig().public.events?.pages;
8
- const { data: events, refresh } = await useFetch("/api/events/admin/events", { default: () => [] });
9
- const eventTo = (id) => (pages?.adminEvent || "/admin/events/:id").replace(":id", encodeURIComponent(id));
10
- const when = (value) => new Intl.DateTimeFormat(locale.value, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value));
11
- const statusColor = { draft: "neutral", published: "success", archived: "warning" };
12
- const columns = [
13
- { accessorKey: "name", header: t("admin.name", "Name"), cell: ({ row }) => tr(row.original.name) },
14
- { accessorKey: "status", header: t("admin.status", "Status") },
15
- { accessorKey: "startsAt", header: t("admin.starts", "Starts"), cell: ({ row }) => when(row.original.startsAt) },
16
- { accessorKey: "location", header: t("admin.location", "Location"), cell: ({ row }) => tr(row.original.location) },
17
- { id: "actions", header: "" }
18
- ];
19
- async function clone(id) {
20
- try {
21
- const copy = await $fetch(`/api/events/admin/events/${id}/clone`, { method: "POST" });
22
- await navigateTo(eventTo(copy.id));
23
- } catch (cause) {
24
- toast.add({ title: cause.statusMessage ?? t("admin.error", "Something went wrong."), color: "error" });
5
+ const format = useFormat();
6
+ const links = useEventLinks();
7
+ 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");
11
+ const RowMenu = resolveComponent("RowMenu");
12
+ const TableIdentity = resolveComponent("TableIdentity");
13
+ const columns = computed(() => [
14
+ {
15
+ id: "name",
16
+ accessorFn: (row) => tr(row.name),
17
+ header: header("name", t("admin.name", "Name")),
18
+ cell: ({ row }) => h(TableIdentity, { title: tr(row.original.name), description: tr(row.original.location) })
19
+ },
20
+ {
21
+ accessorKey: "status",
22
+ 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) })
24
+ },
25
+ {
26
+ accessorKey: "startsAt",
27
+ header: header("startsAt", t("admin.starts", "Starts")),
28
+ cell: ({ row }) => row.original.startsAt ? h("span", { class: "tabular-nums" }, format.dateTime(row.original.startsAt)) : emptyCell()
29
+ },
30
+ {
31
+ id: "actions",
32
+ meta: { class: { th: "w-0", td: "w-0" } },
33
+ cell: ({ row }) => h(RowMenu, { items: actions.menu(row.original, { onRemoved: refresh }), subject: tr(row.original.name) })
25
34
  }
26
- }
27
- const creating = ref(false);
28
- const saving = ref(false);
29
- const draft = reactive({ name: void 0, slug: "", startsAt: "", location: void 0 });
30
- const slugify = (value) => value.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
31
- const draftName = computed(() => tr(draft.name));
32
- watch(draftName, (name, previous) => {
33
- if (draft.slug === slugify(previous)) draft.slug = slugify(name);
34
- });
35
+ ]);
35
36
  async function create() {
36
- saving.value = true;
37
- try {
38
- const created = await $fetch("/api/events/admin/events", {
39
- method: "POST",
40
- body: { name: draft.name, slug: draft.slug, startsAt: draft.startsAt, location: draft.location || null, status: "draft" }
41
- });
42
- await refresh();
43
- await navigateTo(eventTo(created.id));
44
- } catch (cause) {
45
- toast.add({ title: cause.statusMessage ?? t("admin.error", "Something went wrong."), color: "error" });
46
- } finally {
47
- saving.value = false;
48
- }
37
+ const created = await actions.create();
38
+ if (created) await navigateTo(links.adminEvent(created.id));
49
39
  }
50
40
  </script>
51
41
 
52
42
  <template>
53
- <AdminPage>
54
- <AdminPageHeader :title="t('admin.title', 'Events')">
55
- <template #links>
43
+ <div class="flex flex-col gap-4">
44
+ <PageHeading
45
+ :title="t('admin.title', 'Events')"
46
+ :description="t('admin.listDescription', 'Events, their ticket types and attendees.')"
47
+ >
48
+ <template #actions>
56
49
  <UButton
57
50
  icon="i-lucide-plus"
58
51
  :label="t('admin.create', 'New event')"
59
- @click="creating = true"
52
+ @click="create"
60
53
  />
61
54
  </template>
62
- </AdminPageHeader>
55
+ </PageHeading>
63
56
 
64
- <AdminTable
65
- :data="events"
66
- :columns="columns"
67
- @select="(_, row2) => navigateTo(eventTo(row2.original.id))"
57
+ <TableCard
58
+ :loading="status === 'pending' && !events.length"
59
+ :refreshing="status === 'pending' && !!events.length"
68
60
  >
69
- <template #status-cell="{ row }">
70
- <UBadge
71
- variant="subtle"
72
- :color="statusColor[row.original.status]"
73
- :label="row.original.status"
74
- />
75
- </template>
76
- <template #actions-cell="{ row }">
77
- <UButton
78
- color="neutral"
79
- variant="ghost"
80
- size="sm"
81
- icon="i-lucide-copy"
82
- :aria-label="t('admin.clone', 'Duplicate')"
83
- @click.stop="clone(row.original.id)"
84
- />
85
- </template>
86
- <template #empty>
87
- <AdminEmpty
88
- icon="i-lucide-calendar"
89
- :title="t('admin.empty', 'No events yet')"
90
- :description="t('admin.emptyDescription', 'Create the first event to start selling tickets.')"
91
- />
92
- </template>
93
- </AdminTable>
94
-
95
- <UModal
96
- v-model:open="creating"
97
- :title="t('admin.create', 'New event')"
98
- >
99
- <template #body>
100
- <form
101
- id="admin-event-form"
102
- class="flex flex-col gap-4"
103
- @submit.prevent="create"
104
- >
105
- <UFormField :label="t('admin.name', 'Name')">
106
- <TranslatedInput
107
- v-model="draft.name"
108
- required
109
- />
110
- </UFormField>
111
- <UFormField
112
- :label="t('admin.slug', 'URL name')"
113
- :description="t('admin.slugHelp', 'Lowercase letters, numbers and dashes.')"
114
- >
115
- <UInput
116
- v-model="draft.slug"
117
- class="w-full"
118
- required
119
- pattern="[a-z0-9]+(-[a-z0-9]+)*"
120
- />
121
- </UFormField>
122
- <UFormField :label="t('admin.starts', 'Starts')">
123
- <UInput
124
- v-model="draft.startsAt"
125
- type="datetime-local"
126
- class="w-full"
127
- required
128
- />
129
- </UFormField>
130
- <UFormField :label="t('admin.location', 'Location')">
131
- <TranslatedInput v-model="draft.location" />
132
- </UFormField>
133
- </form>
134
- </template>
135
- <template #footer>
136
- <div class="flex w-full justify-end gap-2">
137
- <UButton
138
- variant="subtle"
139
- color="neutral"
140
- :label="t('admin.cancel', 'Cancel')"
141
- @click="creating = false"
142
- />
143
- <UButton
144
- type="submit"
145
- form="admin-event-form"
146
- :loading="saving"
147
- :disabled="!draftName"
148
- :label="t('admin.createAction', 'Create draft')"
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))"
67
+ >
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 }]"
149
75
  />
150
- </div>
151
- </template>
152
- </UModal>
153
- </AdminPage>
76
+ </template>
77
+ </UTable>
78
+ </TableCard>
79
+ </div>
154
80
  </template>
@@ -1,9 +1,9 @@
1
- declare var __VLS_82: {
1
+ declare var __VLS_91: {
2
2
  event: any;
3
3
  body: any;
4
4
  };
5
5
  type __VLS_Slots = {} & {
6
- body?: (props: typeof __VLS_82) => any;
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>;