@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.
- package/README.md +1 -1
- package/dist/module.d.mts +45 -7
- package/dist/module.json +1 -1
- package/dist/module.mjs +53 -42
- package/dist/runtime/app/components/EventAdminAddonDialog.d.vue.ts +11 -0
- package/dist/runtime/app/components/EventAdminAddonDialog.vue +113 -0
- package/dist/runtime/app/components/EventAdminAddonDialog.vue.d.ts +11 -0
- package/dist/runtime/app/components/EventAdminAddonGroupDialog.d.vue.ts +11 -0
- package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue +79 -0
- package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue.d.ts +11 -0
- package/dist/runtime/app/components/EventAdminAddonGroups.d.vue.ts +19 -0
- package/dist/runtime/app/components/EventAdminAddonGroups.vue +123 -0
- package/dist/runtime/app/components/EventAdminAddonGroups.vue.d.ts +19 -0
- package/dist/runtime/app/components/EventAdminTicketTypeDialog.d.vue.ts +20 -0
- package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue +175 -0
- package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue.d.ts +20 -0
- package/dist/runtime/app/components/EventCard.vue +39 -27
- package/dist/runtime/app/components/EventTicketCard.vue +5 -4
- package/dist/runtime/app/components/EventTicketPicker.vue +40 -21
- package/dist/runtime/app/composables/index.d.ts +1 -1
- package/dist/runtime/app/composables/index.js +1 -1
- package/dist/runtime/app/composables/useEvents.d.ts +10 -4
- package/dist/runtime/app/composables/useEvents.js +21 -10
- package/dist/runtime/app/pages/admin-event.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-event.vue +151 -0
- package/dist/runtime/app/pages/admin-event.vue.d.ts +3 -0
- package/dist/runtime/app/pages/admin.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin.vue +154 -0
- package/dist/runtime/app/pages/admin.vue.d.ts +3 -0
- package/dist/runtime/app/pages/event.d.vue.ts +14 -1
- package/dist/runtime/app/pages/event.vue +160 -45
- package/dist/runtime/app/pages/event.vue.d.ts +14 -1
- package/dist/runtime/app/pages/events.vue +12 -10
- package/dist/runtime/app/pages/tickets.vue +26 -19
- package/dist/runtime/app/tabs/Attendees.d.vue.ts +7 -0
- package/dist/runtime/app/tabs/Attendees.vue +88 -0
- package/dist/runtime/app/tabs/Attendees.vue.d.ts +7 -0
- package/dist/runtime/app/tabs/Details.d.vue.ts +11 -0
- package/dist/runtime/app/tabs/Details.vue +74 -0
- package/dist/runtime/app/tabs/Details.vue.d.ts +11 -0
- package/dist/runtime/app/tabs/Tickets.d.vue.ts +11 -0
- package/dist/runtime/app/tabs/Tickets.vue +164 -0
- package/dist/runtime/app/tabs/Tickets.vue.d.ts +11 -0
- package/dist/runtime/app/utils/admin-event.d.ts +48 -0
- package/dist/runtime/app/utils/admin-event.js +22 -0
- package/dist/runtime/i18n/lang/da.json +34 -1
- package/dist/runtime/i18n/lang/en.json +33 -0
- package/dist/runtime/menus.js +17 -1
- package/dist/runtime/server/db/schema.sqlite.d.ts +62 -5
- package/dist/runtime/server/db/schema.sqlite.js +27 -2
- package/dist/runtime/server/mcp/schemas.d.ts +119 -0
- package/dist/runtime/server/mcp/schemas.js +58 -0
- package/dist/runtime/server/mcp/tools/admin/create-event.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/create-event.js +11 -0
- package/dist/runtime/server/mcp/tools/admin/get-event-definition.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/get-event-definition.js +10 -0
- package/dist/runtime/server/mcp/tools/admin/list-all-events.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/list-all-events.js +9 -0
- package/dist/runtime/server/mcp/tools/admin/list-attendees.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/list-attendees.js +25 -0
- package/dist/runtime/server/mcp/tools/admin/update-event.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/update-event.js +10 -0
- package/dist/runtime/server/mcp/tools/admin/update-ticket-type.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/update-ticket-type.js +10 -0
- package/dist/runtime/server/mcp/tools/get-event.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/get-event.js +12 -0
- package/dist/runtime/server/mcp/tools/list-events.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/list-events.js +6 -0
- package/dist/runtime/server/mcp/tools/my-tickets.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/my-tickets.js +6 -0
- package/dist/runtime/server/plugins/staff-scopes.d.ts +2 -0
- package/dist/runtime/server/plugins/staff-scopes.js +1 -0
- package/dist/runtime/server/plugins/subscriptions-membership.d.ts +2 -0
- package/dist/runtime/server/plugins/subscriptions-membership.js +11 -0
- package/dist/runtime/server/routes/admin/event-clone.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/event-clone.post.js +31 -0
- package/dist/runtime/server/routes/admin/event.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/event.get.js +5 -0
- package/dist/runtime/server/routes/admin/event.patch.js +3 -8
- package/dist/runtime/server/routes/admin/events.post.js +3 -13
- package/dist/runtime/server/routes/admin/membership-options.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/membership-options.get.js +4 -0
- package/dist/runtime/server/routes/admin/ticket-type.patch.js +3 -8
- package/dist/runtime/server/routes/admin/ticket-types.post.js +2 -1
- package/dist/runtime/server/routes/public/event.get.js +2 -2
- package/dist/runtime/server/routes/public/purchase.post.js +3 -2
- package/dist/runtime/server/routes/public/ticket-pay.post.js +1 -1
- package/dist/runtime/server/routes/public/ticket.get.js +1 -1
- package/dist/runtime/server/routes/public/tickets-me.get.js +1 -1
- package/dist/runtime/server/utils/admin-schemas.d.ts +102 -20
- package/dist/runtime/server/utils/admin-schemas.js +8 -2
- package/dist/runtime/server/utils/admin.d.ts +82 -1
- package/dist/runtime/server/utils/admin.js +51 -1
- package/dist/runtime/server/utils/auth.d.ts +7 -6
- package/dist/runtime/server/utils/auth.js +4 -4
- package/dist/runtime/server/utils/catalog.d.ts +14 -0
- package/dist/runtime/server/utils/catalog.js +20 -4
- package/dist/runtime/server/utils/membership.d.ts +6 -2
- package/dist/runtime/server/utils/membership.js +15 -2
- package/dist/runtime/server/utils/tickets.js +15 -12
- package/dist/runtime/utils/index.d.ts +17 -7
- package/dist/runtime/utils/index.js +16 -19
- package/dist/types.d.mts +3 -1
- package/package.json +21 -7
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
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 ?
|
|
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-
|
|
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
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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>
|