@frigear-nu/events 0.0.6 → 0.0.8

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 (104) hide show
  1. package/README.md +1 -1
  2. package/dist/module.d.mts +45 -7
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +53 -42
  5. package/dist/runtime/app/components/EventAdminAddonDialog.d.vue.ts +11 -0
  6. package/dist/runtime/app/components/EventAdminAddonDialog.vue +113 -0
  7. package/dist/runtime/app/components/EventAdminAddonDialog.vue.d.ts +11 -0
  8. package/dist/runtime/app/components/EventAdminAddonGroupDialog.d.vue.ts +11 -0
  9. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue +79 -0
  10. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue.d.ts +11 -0
  11. package/dist/runtime/app/components/EventAdminAddonGroups.d.vue.ts +19 -0
  12. package/dist/runtime/app/components/EventAdminAddonGroups.vue +123 -0
  13. package/dist/runtime/app/components/EventAdminAddonGroups.vue.d.ts +19 -0
  14. package/dist/runtime/app/components/EventAdminTicketTypeDialog.d.vue.ts +20 -0
  15. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue +175 -0
  16. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue.d.ts +20 -0
  17. package/dist/runtime/app/components/EventCard.vue +39 -27
  18. package/dist/runtime/app/components/EventTicketCard.vue +5 -4
  19. package/dist/runtime/app/components/EventTicketPicker.vue +40 -21
  20. package/dist/runtime/app/composables/index.d.ts +1 -1
  21. package/dist/runtime/app/composables/index.js +1 -1
  22. package/dist/runtime/app/composables/useEvents.d.ts +10 -4
  23. package/dist/runtime/app/composables/useEvents.js +21 -10
  24. package/dist/runtime/app/pages/admin-event.d.vue.ts +3 -0
  25. package/dist/runtime/app/pages/admin-event.vue +151 -0
  26. package/dist/runtime/app/pages/admin-event.vue.d.ts +3 -0
  27. package/dist/runtime/app/pages/admin.d.vue.ts +3 -0
  28. package/dist/runtime/app/pages/admin.vue +154 -0
  29. package/dist/runtime/app/pages/admin.vue.d.ts +3 -0
  30. package/dist/runtime/app/pages/event.d.vue.ts +14 -1
  31. package/dist/runtime/app/pages/event.vue +160 -45
  32. package/dist/runtime/app/pages/event.vue.d.ts +14 -1
  33. package/dist/runtime/app/pages/events.vue +12 -10
  34. package/dist/runtime/app/pages/tickets.vue +26 -19
  35. package/dist/runtime/app/tabs/Attendees.d.vue.ts +7 -0
  36. package/dist/runtime/app/tabs/Attendees.vue +88 -0
  37. package/dist/runtime/app/tabs/Attendees.vue.d.ts +7 -0
  38. package/dist/runtime/app/tabs/Details.d.vue.ts +11 -0
  39. package/dist/runtime/app/tabs/Details.vue +74 -0
  40. package/dist/runtime/app/tabs/Details.vue.d.ts +11 -0
  41. package/dist/runtime/app/tabs/Tickets.d.vue.ts +11 -0
  42. package/dist/runtime/app/tabs/Tickets.vue +164 -0
  43. package/dist/runtime/app/tabs/Tickets.vue.d.ts +11 -0
  44. package/dist/runtime/app/utils/admin-event.d.ts +48 -0
  45. package/dist/runtime/app/utils/admin-event.js +22 -0
  46. package/dist/runtime/i18n/lang/da.json +34 -1
  47. package/dist/runtime/i18n/lang/en.json +33 -0
  48. package/dist/runtime/menus.js +17 -1
  49. package/dist/runtime/server/db/schema.sqlite.d.ts +62 -5
  50. package/dist/runtime/server/db/schema.sqlite.js +27 -2
  51. package/dist/runtime/server/mcp/schemas.d.ts +119 -0
  52. package/dist/runtime/server/mcp/schemas.js +58 -0
  53. package/dist/runtime/server/mcp/tools/admin/create-event.d.ts +2 -0
  54. package/dist/runtime/server/mcp/tools/admin/create-event.js +11 -0
  55. package/dist/runtime/server/mcp/tools/admin/get-event-definition.d.ts +2 -0
  56. package/dist/runtime/server/mcp/tools/admin/get-event-definition.js +10 -0
  57. package/dist/runtime/server/mcp/tools/admin/list-all-events.d.ts +2 -0
  58. package/dist/runtime/server/mcp/tools/admin/list-all-events.js +9 -0
  59. package/dist/runtime/server/mcp/tools/admin/list-attendees.d.ts +2 -0
  60. package/dist/runtime/server/mcp/tools/admin/list-attendees.js +25 -0
  61. package/dist/runtime/server/mcp/tools/admin/update-event.d.ts +2 -0
  62. package/dist/runtime/server/mcp/tools/admin/update-event.js +10 -0
  63. package/dist/runtime/server/mcp/tools/admin/update-ticket-type.d.ts +2 -0
  64. package/dist/runtime/server/mcp/tools/admin/update-ticket-type.js +10 -0
  65. package/dist/runtime/server/mcp/tools/get-event.d.ts +2 -0
  66. package/dist/runtime/server/mcp/tools/get-event.js +12 -0
  67. package/dist/runtime/server/mcp/tools/list-events.d.ts +2 -0
  68. package/dist/runtime/server/mcp/tools/list-events.js +6 -0
  69. package/dist/runtime/server/mcp/tools/my-tickets.d.ts +2 -0
  70. package/dist/runtime/server/mcp/tools/my-tickets.js +6 -0
  71. package/dist/runtime/server/plugins/staff-scopes.d.ts +2 -0
  72. package/dist/runtime/server/plugins/staff-scopes.js +1 -0
  73. package/dist/runtime/server/plugins/subscriptions-membership.d.ts +2 -0
  74. package/dist/runtime/server/plugins/subscriptions-membership.js +11 -0
  75. package/dist/runtime/server/routes/admin/event-clone.post.d.ts +2 -0
  76. package/dist/runtime/server/routes/admin/event-clone.post.js +31 -0
  77. package/dist/runtime/server/routes/admin/event.get.d.ts +2 -0
  78. package/dist/runtime/server/routes/admin/event.get.js +5 -0
  79. package/dist/runtime/server/routes/admin/event.patch.js +3 -8
  80. package/dist/runtime/server/routes/admin/events.post.js +3 -13
  81. package/dist/runtime/server/routes/admin/membership-options.get.d.ts +2 -0
  82. package/dist/runtime/server/routes/admin/membership-options.get.js +4 -0
  83. package/dist/runtime/server/routes/admin/ticket-type.patch.js +3 -8
  84. package/dist/runtime/server/routes/admin/ticket-types.post.js +2 -1
  85. package/dist/runtime/server/routes/public/event.get.js +2 -2
  86. package/dist/runtime/server/routes/public/purchase.post.js +3 -2
  87. package/dist/runtime/server/routes/public/ticket-pay.post.js +1 -1
  88. package/dist/runtime/server/routes/public/ticket.get.js +1 -1
  89. package/dist/runtime/server/routes/public/tickets-me.get.js +1 -1
  90. package/dist/runtime/server/utils/admin-schemas.d.ts +102 -20
  91. package/dist/runtime/server/utils/admin-schemas.js +8 -2
  92. package/dist/runtime/server/utils/admin.d.ts +82 -1
  93. package/dist/runtime/server/utils/admin.js +51 -1
  94. package/dist/runtime/server/utils/auth.d.ts +7 -6
  95. package/dist/runtime/server/utils/auth.js +4 -4
  96. package/dist/runtime/server/utils/catalog.d.ts +14 -0
  97. package/dist/runtime/server/utils/catalog.js +20 -4
  98. package/dist/runtime/server/utils/membership.d.ts +6 -2
  99. package/dist/runtime/server/utils/membership.js +15 -2
  100. package/dist/runtime/server/utils/tickets.js +15 -12
  101. package/dist/runtime/utils/index.d.ts +17 -7
  102. package/dist/runtime/utils/index.js +16 -19
  103. package/dist/types.d.mts +3 -1
  104. package/package.json +21 -7
@@ -1,16 +1,18 @@
1
- export declare function useEvents(): {
1
+ import type { Translated } from '../../../types.js';
2
+ export declare function useEvents(): Promise<{
2
3
  events: any;
3
4
  grouped: any;
4
5
  status: any;
5
6
  refresh: any;
6
- };
7
- export declare function useEvent(slug: MaybeRefOrGetter<string>): {
7
+ }>;
8
+ /** Awaitable: resolves once the event is fetched, so SSR renders the page with data. */
9
+ export declare function useEvent(slug: MaybeRefOrGetter<string>): Promise<{
8
10
  event: any;
9
11
  status: any;
10
12
  error: any;
11
13
  refresh: any;
12
14
  purchase: (ticketTypeId: string, addonIds?: string[]) => Promise<any>;
13
- };
15
+ }>;
14
16
  export declare function useMyTickets(): {
15
17
  tickets: any;
16
18
  status: any;
@@ -19,3 +21,7 @@ export declare function useMyTickets(): {
19
21
  };
20
22
  /** Locale for translated texts: `@nuxtjs/i18n`'s when installed, otherwise `en`. */
21
23
  export declare function useEventsLocale(): any;
24
+ /** The site's default locale: shown for texts that have no translation in the current locale. */
25
+ export declare function useEventsFallbackLocale(): any;
26
+ /** `tr(value)`: a translated text in the current locale, falling back to the default locale, then `en`, then any. */
27
+ export declare function useEventsTranslate(): (value: Translated | null | undefined) => string;
@@ -1,28 +1,30 @@
1
- export function useEvents() {
2
- const { data, status, refresh } = useFetch("/api/events", { key: "events:list", default: () => [] });
1
+ import { translate } from "../../utils/index.js";
2
+ export async function useEvents() {
3
+ const now = useState("events:now", () => Date.now());
4
+ const { data, status, refresh } = await useFetch("/api/events", { key: "events:list", default: () => [] });
3
5
  const grouped = computed(() => {
4
- const now = Date.now();
5
6
  const start = (e) => new Date(e.startsAt).getTime();
6
7
  const end = (e) => e.endsAt ? new Date(e.endsAt).getTime() : start(e) + 24 * 36e5;
7
8
  return {
8
- ongoing: data.value.filter((e) => start(e) <= now && now < end(e)),
9
- upcoming: data.value.filter((e) => start(e) > now),
10
- past: data.value.filter((e) => end(e) <= now).reverse()
9
+ ongoing: data.value.filter((e) => start(e) <= now.value && now.value < end(e)),
10
+ upcoming: data.value.filter((e) => start(e) > now.value),
11
+ past: data.value.filter((e) => end(e) <= now.value).reverse()
11
12
  };
12
13
  });
13
14
  return { events: data, grouped, status, refresh };
14
15
  }
15
- export function useEvent(slug) {
16
+ export async function useEvent(slug) {
16
17
  const { loggedIn } = useUserSession();
17
- const { data, status, error, refresh } = useFetch(() => `/api/events/${toValue(slug)}`, {
18
+ const locale = useEventsLocale();
19
+ const { data, status, error, refresh } = await useFetch(() => `/api/events/${toValue(slug)}`, {
18
20
  key: `events:detail:${toValue(slug)}`,
19
- // Eligibility (membership, limits) depends on the session.
21
+ // Eligibility (membership, limits) depends on the session; refetch quietly, keeping the data on screen.
20
22
  watch: [loggedIn]
21
23
  });
22
24
  async function purchase(ticketTypeId, addonIds = []) {
23
25
  const result = await $fetch(`/api/events/${toValue(slug)}/purchase`, {
24
26
  method: "POST",
25
- body: { ticketTypeId, addonIds }
27
+ body: { ticketTypeId, addonIds, locale: locale.value }
26
28
  });
27
29
  if (result.url) await navigateTo(result.url, { external: true });
28
30
  else await refresh();
@@ -48,3 +50,12 @@ export function useEventsLocale() {
48
50
  const nuxtApp = useNuxtApp();
49
51
  return computed(() => nuxtApp.$i18n?.locale?.value ?? "en");
50
52
  }
53
+ export function useEventsFallbackLocale() {
54
+ const nuxtApp = useNuxtApp();
55
+ return computed(() => nuxtApp.$i18n?.defaultLocale ?? "en");
56
+ }
57
+ export function useEventsTranslate() {
58
+ const locale = useEventsLocale();
59
+ const fallback = useEventsFallbackLocale();
60
+ return (value) => translate(value, locale.value, fallback.value);
61
+ }
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,151 @@
1
+ <script setup>
2
+ import AttendeesTab from "../tabs/Attendees.vue";
3
+ import DetailsTab from "../tabs/Details.vue";
4
+ import TicketsTab from "../tabs/Tickets.vue";
5
+ import { toLocalInput, useAdminAction } from "../utils/admin-event";
6
+ defineOptions({ name: "EventsAdminEventPage" });
7
+ const t = useModuleT("events");
8
+ const route = useRoute();
9
+ const tr = useEventsTranslate();
10
+ const pages = useRuntimeConfig().public.events?.pages;
11
+ const id = computed(() => String(route.params.id));
12
+ const { busy, run } = useAdminAction();
13
+ const { data: event, refresh } = await useFetch(() => `/api/events/admin/events/${id.value}`);
14
+ const Tabs = resolveComponent("WidgetTabs");
15
+ const tabs = typeof Tabs === "string" ? void 0 : Tabs;
16
+ const facts = reactive({
17
+ status: "draft",
18
+ startsAt: "",
19
+ endsAt: "",
20
+ location: void 0,
21
+ slug: ""
22
+ });
23
+ watch(event, (value) => {
24
+ if (!value) return;
25
+ Object.assign(facts, {
26
+ status: value.status,
27
+ startsAt: toLocalInput(value.startsAt),
28
+ endsAt: toLocalInput(value.endsAt),
29
+ location: value.location ?? void 0,
30
+ slug: value.slug
31
+ });
32
+ }, { immediate: true });
33
+ 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
+ const saveFacts = () => run(async () => {
36
+ await $fetch(`/api/events/admin/events/${id.value}`, {
37
+ method: "PATCH",
38
+ body: {
39
+ status: facts.status,
40
+ startsAt: facts.startsAt,
41
+ endsAt: facts.endsAt || null,
42
+ location: facts.location ?? null,
43
+ slug: facts.slug
44
+ }
45
+ });
46
+ await refresh();
47
+ }, t("admin.saved", "Saved."));
48
+ </script>
49
+
50
+ <template>
51
+ <AdminPage v-if="event">
52
+ <AdminPageHeader
53
+ :title="tr(event.name)"
54
+ :back="{ label: t('admin.title', 'Events'), to: pages?.admin || '/admin/events' }"
55
+ >
56
+ <UBadge
57
+ variant="subtle"
58
+ :color="statusColor[event.status] ?? 'neutral'"
59
+ :label="t(`admin.statuses.${event.status}`, event.status)"
60
+ />
61
+ <template #actions>
62
+ <UButton
63
+ v-if="event.status === 'published' && pages?.event"
64
+ :to="pages.event.replace(':slug', encodeURIComponent(event.slug))"
65
+ variant="outline"
66
+ color="neutral"
67
+ icon="i-lucide-external-link"
68
+ :label="t('admin.viewPublic', 'View public page')"
69
+ />
70
+ </template>
71
+ </AdminPageHeader>
72
+
73
+ <AdminSection :title="t('admin.keyFacts', 'Event')">
74
+ <form
75
+ id="admin-event-facts"
76
+ class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"
77
+ @submit.prevent="saveFacts"
78
+ >
79
+ <UFormField :label="t('admin.status', 'Status')">
80
+ <USelect
81
+ v-model="facts.status"
82
+ :items="statuses"
83
+ class="w-full"
84
+ />
85
+ </UFormField>
86
+ <UFormField :label="t('admin.starts', 'Starts')">
87
+ <UInput
88
+ v-model="facts.startsAt"
89
+ type="datetime-local"
90
+ class="w-full"
91
+ required
92
+ />
93
+ </UFormField>
94
+ <UFormField :label="t('admin.ends', 'Ends')">
95
+ <UInput
96
+ v-model="facts.endsAt"
97
+ type="datetime-local"
98
+ class="w-full"
99
+ />
100
+ </UFormField>
101
+ <UFormField :label="t('admin.location', 'Location')">
102
+ <TranslatedInput v-model="facts.location" />
103
+ </UFormField>
104
+ <UFormField :label="t('admin.slug', 'URL name')">
105
+ <UInput
106
+ v-model="facts.slug"
107
+ class="w-full"
108
+ required
109
+ />
110
+ </UFormField>
111
+ </form>
112
+ <template #footer>
113
+ <div class="flex justify-end">
114
+ <UButton
115
+ type="submit"
116
+ form="admin-event-facts"
117
+ :loading="busy"
118
+ :label="t('admin.save', 'Save')"
119
+ />
120
+ </div>
121
+ </template>
122
+ </AdminSection>
123
+
124
+ <component
125
+ :is="tabs"
126
+ v-if="tabs"
127
+ name="admin.events"
128
+ :context="{ event }"
129
+ @refresh="refresh"
130
+ />
131
+ <template v-else>
132
+ <DetailsTab
133
+ :event="event"
134
+ @refresh="refresh"
135
+ />
136
+ <TicketsTab
137
+ :event="event"
138
+ @refresh="refresh"
139
+ />
140
+ <AttendeesTab
141
+ :event="event"
142
+ @refresh="refresh"
143
+ />
144
+ </template>
145
+ </AdminPage>
146
+ <AdminEmpty
147
+ v-else
148
+ icon="i-lucide-calendar-x"
149
+ :title="t('notFound', 'Event not found')"
150
+ />
151
+ </template>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,154 @@
1
+ <script setup>
2
+ defineOptions({ name: "EventsAdminPage" });
3
+ const t = useModuleT("events");
4
+ const toast = useToast();
5
+ const locale = useEventsLocale();
6
+ 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" });
25
+ }
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
+ 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
+ }
49
+ }
50
+ </script>
51
+
52
+ <template>
53
+ <AdminPage>
54
+ <AdminPageHeader :title="t('admin.title', 'Events')">
55
+ <template #links>
56
+ <UButton
57
+ icon="i-lucide-plus"
58
+ :label="t('admin.create', 'New event')"
59
+ @click="creating = true"
60
+ />
61
+ </template>
62
+ </AdminPageHeader>
63
+
64
+ <AdminTable
65
+ :data="events"
66
+ :columns="columns"
67
+ @select="(_, row2) => navigateTo(eventTo(row2.original.id))"
68
+ >
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')"
149
+ />
150
+ </div>
151
+ </template>
152
+ </UModal>
153
+ </AdminPage>
154
+ </template>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -1,3 +1,16 @@
1
- declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
1
+ declare var __VLS_82: {
2
+ event: any;
3
+ body: any;
4
+ };
5
+ type __VLS_Slots = {} & {
6
+ body?: (props: typeof __VLS_82) => any;
7
+ };
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
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
2
10
  declare const _default: typeof __VLS_export;
3
11
  export default _default;
12
+ type __VLS_WithSlots<T, S> = T & {
13
+ new (): {
14
+ $slots: S;
15
+ };
16
+ };
@@ -1,15 +1,17 @@
1
1
  <script setup>
2
- import { translate } from "../../utils";
2
+ const hasMdc = typeof resolveComponent("MDC") !== "string";
3
3
  const t = useModuleT("events");
4
4
  const route = useRoute();
5
5
  const slug = computed(() => String(route.params.slug));
6
6
  const { loggedIn } = useUserSession();
7
- const { event, status, error, purchase, refresh } = useEvent(slug);
7
+ const { event, error, purchase, refresh } = await useEvent(slug);
8
8
  const locale = useEventsLocale();
9
+ const tr = useEventsTranslate();
9
10
  const toast = useToast();
10
- if (error.value?.statusCode === 404) {
11
- throw createError({ statusCode: 404, statusMessage: t("notFound", "Event not found"), fatal: true });
11
+ if (!event.value) {
12
+ throw createError({ statusCode: error.value?.statusCode ?? 404, statusMessage: t("notFound", "Event not found"), fatal: true });
12
13
  }
14
+ const eventsPath = useRuntimeConfig().public.events?.pages?.events || "/events";
13
15
  const buying = ref(false);
14
16
  async function onPurchase(ticketTypeId, addonIds) {
15
17
  buying.value = true;
@@ -40,12 +42,12 @@ onMounted(async () => {
40
42
  paidState.value = "timeout";
41
43
  });
42
44
  const ticketsPath = useRuntimeConfig().public.events?.pages?.tickets;
43
- useSeoMeta({ title: () => event.value ? translate(event.value.name, locale.value) : "Event" });
45
+ useSeoMeta({ title: () => event.value ? tr(event.value.name) : "Event" });
44
46
  defineOptions({ name: "EventDetailPage" });
45
47
  </script>
46
48
 
47
49
  <template>
48
- <UContainer class="py-10">
50
+ <UContainer class="py-8 sm:py-12">
49
51
  <template v-if="event">
50
52
  <UAlert
51
53
  v-if="paidState === 'waiting'"
@@ -88,48 +90,161 @@ defineOptions({ name: "EventDetailPage" });
88
90
  :title="t('paymentCancelled', 'Payment cancelled. Your reservation is held for a short while; you can pay from your tickets.')"
89
91
  />
90
92
 
91
- <img
92
- v-if="event.image"
93
- :src="event.image"
94
- :alt="translate(event.name, locale)"
95
- class="mb-6 aspect-[3/1] w-full rounded-lg object-cover"
96
- >
97
- <h1 class="text-3xl font-semibold">
98
- {{ translate(event.name, locale) }}
99
- </h1>
100
- <p class="mt-2 text-muted">
101
- {{ new Intl.DateTimeFormat(locale, { dateStyle: "full", timeStyle: "short" }).format(new Date(event.startsAt)) }}
102
- <template v-if="event.location">
103
- · <a
104
- v-if="event.locationUrl"
105
- :href="event.locationUrl"
106
- target="_blank"
107
- rel="noopener"
108
- class="underline"
109
- >{{ event.location }}</a><template v-else>
110
- {{ event.location }}
111
- </template>
112
- </template>
113
- </p>
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
+ />
114
102
 
115
- <div class="mt-8 grid gap-8 md:grid-cols-[1fr_24rem]">
116
- <!-- Plain text body: markdown rendering is left to the host (e.g. via a typography module). -->
117
- <div class="whitespace-pre-line">
118
- {{ translate(event.body ?? event.description, locale) }}
119
- </div>
120
- <EventTicketPicker
121
- :ticket-types="event.ticketTypes"
122
- :logged-in="loggedIn"
123
- :loading="buying"
124
- @purchase="onPurchase"
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]"
125
114
  />
126
115
  </div>
116
+
117
+ <header class="mt-8 space-y-4">
118
+ <div
119
+ v-if="event.requiresMembership"
120
+ class="flex flex-wrap items-center gap-2"
121
+ >
122
+ <UBadge
123
+ color="warning"
124
+ variant="subtle"
125
+ icon="i-lucide-user-star"
126
+ :label="t('members', 'Members')"
127
+ />
128
+ </div>
129
+ <h1 class="text-3xl font-bold tracking-tight text-highlighted sm:text-5xl">
130
+ {{ tr(event.name) }}
131
+ </h1>
132
+ <p
133
+ v-if="event.description"
134
+ class="max-w-3xl text-lg text-muted"
135
+ >
136
+ {{ tr(event.description) }}
137
+ </p>
138
+
139
+ <div class="grid gap-3 pt-2 sm:grid-cols-2">
140
+ <div class="flex items-start gap-3 rounded-xl bg-elevated/50 p-4 ring ring-default">
141
+ <UIcon
142
+ name="i-lucide-calendar"
143
+ class="mt-0.5 size-5 shrink-0 text-primary"
144
+ />
145
+ <div class="min-w-0">
146
+ <div class="text-xs font-semibold uppercase tracking-wide text-muted">
147
+ {{ t("detail.when", "When?") }}
148
+ </div>
149
+ <div class="font-medium text-highlighted">
150
+ <NuxtTime
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>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ <component
168
+ :is="event.locationUrl ? 'a' : 'div'"
169
+ v-if="event.location"
170
+ :href="event.locationUrl || void 0"
171
+ :target="event.locationUrl ? '_blank' : void 0"
172
+ :rel="event.locationUrl ? 'noopener' : void 0"
173
+ class="flex items-start gap-3 rounded-xl bg-elevated/50 p-4 ring ring-default"
174
+ :class="event.locationUrl ? 'transition hover:bg-elevated' : ''"
175
+ >
176
+ <UIcon
177
+ name="i-lucide-map-pin"
178
+ class="mt-0.5 size-5 shrink-0 text-primary"
179
+ />
180
+ <div class="min-w-0">
181
+ <div class="text-xs font-semibold uppercase tracking-wide text-muted">
182
+ {{ t("detail.where", "Where?") }}
183
+ </div>
184
+ <div class="flex items-center gap-1.5 font-medium text-highlighted">
185
+ {{ tr(event.location) }}
186
+ <UIcon
187
+ v-if="event.locationUrl"
188
+ name="i-lucide-external-link"
189
+ class="size-4 text-muted"
190
+ />
191
+ </div>
192
+ </div>
193
+ </component>
194
+ </div>
195
+
196
+ <div
197
+ v-if="event.ticketTypes.length"
198
+ class="md:hidden"
199
+ >
200
+ <UButton
201
+ href="#tickets"
202
+ size="lg"
203
+ block
204
+ trailing-icon="i-lucide-ticket"
205
+ :label="t('detail.tickets.view', 'Show tickets')"
206
+ />
207
+ </div>
208
+ </header>
209
+
210
+ <div class="mt-10 grid gap-10 lg:grid-cols-[2fr_1fr] lg:items-start">
211
+ <!-- Body renders as MDC when the host has it (Nuxt Content), else plain text; override via the `body` slot. -->
212
+ <div>
213
+ <slot
214
+ name="body"
215
+ :event="event"
216
+ :body="tr(event.body ?? event.description)"
217
+ >
218
+ <MDC
219
+ v-if="hasMdc"
220
+ :value="tr(event.body ?? event.description)"
221
+ class="prose max-w-none text-muted"
222
+ />
223
+ <div
224
+ v-else
225
+ class="whitespace-pre-line text-muted"
226
+ >
227
+ {{ tr(event.body ?? event.description) }}
228
+ </div>
229
+ </slot>
230
+ </div>
231
+ <UCard
232
+ id="tickets"
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>
127
248
  </template>
128
- <p
129
- v-else-if="status === 'pending'"
130
- class="text-muted"
131
- >
132
- {{ t("loading", "Loading\u2026") }}
133
- </p>
134
249
  </UContainer>
135
250
  </template>