@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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { formatMoney as formatPrice } from "@frigear-nu/ui/money";
|
|
3
|
+
import { useAdminAction } from "../utils/admin-event";
|
|
4
|
+
defineOptions({ name: "EventAdminAddonGroups" });
|
|
5
|
+
defineProps({
|
|
6
|
+
ticketType: { type: Object, required: true }
|
|
7
|
+
});
|
|
8
|
+
const emit = defineEmits(["refresh", "addGroup", "editGroup", "addAddon", "editAddon"]);
|
|
9
|
+
const t = useModuleT("events");
|
|
10
|
+
const locale = useEventsLocale();
|
|
11
|
+
const tr = useEventsTranslate();
|
|
12
|
+
const confirmAction = useConfirmDialog();
|
|
13
|
+
const { run } = useAdminAction();
|
|
14
|
+
const removeGroup = async (addonGroup) => {
|
|
15
|
+
if (!await confirmAction({ title: t("admin.confirmRemoveGroup", "Delete this add-on group and its choices?"), confirmText: t("admin.remove", "Delete"), cancelText: t("admin.cancel", "Cancel"), destructive: true })) return;
|
|
16
|
+
return run(async () => {
|
|
17
|
+
await $fetch(`/api/events/admin/addon-groups/${addonGroup.id}`, { method: "DELETE" });
|
|
18
|
+
emit("refresh");
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
const removeAddon = async (existing) => {
|
|
22
|
+
if (!await confirmAction({ title: t("admin.confirmRemoveAddon", "Delete this choice?"), confirmText: t("admin.remove", "Delete"), cancelText: t("admin.cancel", "Cancel"), destructive: true })) return;
|
|
23
|
+
return run(async () => {
|
|
24
|
+
await $fetch(`/api/events/admin/addons/${existing.id}`, { method: "DELETE" });
|
|
25
|
+
emit("refresh");
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<div class="flex flex-col gap-3 p-2">
|
|
32
|
+
<div
|
|
33
|
+
v-for="addonGroup in ticketType.addonGroups"
|
|
34
|
+
:key="addonGroup.id"
|
|
35
|
+
class="border-default overflow-hidden rounded-lg border bg-default"
|
|
36
|
+
>
|
|
37
|
+
<div class="border-default flex items-center justify-between gap-2 border-b px-3 py-2">
|
|
38
|
+
<div class="min-w-0 text-sm">
|
|
39
|
+
<span class="font-medium">{{ tr(addonGroup.title) }}</span>
|
|
40
|
+
<span class="text-muted ms-2 text-xs">
|
|
41
|
+
{{ addonGroup.required ? t("admin.groupRequired", "required") : t("admin.groupOptional", "optional") }}
|
|
42
|
+
· {{ addonGroup.multiple ? t("admin.groupMultiple", "several choices") : t("admin.groupSingle", "one choice") }}
|
|
43
|
+
</span>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="flex shrink-0 gap-1">
|
|
46
|
+
<UButton
|
|
47
|
+
icon="i-lucide-plus"
|
|
48
|
+
color="neutral"
|
|
49
|
+
variant="ghost"
|
|
50
|
+
size="xs"
|
|
51
|
+
:aria-label="t('admin.addAddon', 'Add choice')"
|
|
52
|
+
@click="emit('addAddon', addonGroup)"
|
|
53
|
+
/>
|
|
54
|
+
<UButton
|
|
55
|
+
icon="i-lucide-pencil"
|
|
56
|
+
color="neutral"
|
|
57
|
+
variant="ghost"
|
|
58
|
+
size="xs"
|
|
59
|
+
:aria-label="t('admin.edit', 'Edit')"
|
|
60
|
+
@click="emit('editGroup', addonGroup)"
|
|
61
|
+
/>
|
|
62
|
+
<UButton
|
|
63
|
+
icon="i-lucide-trash"
|
|
64
|
+
color="error"
|
|
65
|
+
variant="ghost"
|
|
66
|
+
size="xs"
|
|
67
|
+
:aria-label="t('admin.remove', 'Delete')"
|
|
68
|
+
@click="removeGroup(addonGroup)"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
<ul
|
|
73
|
+
v-if="addonGroup.addons.length"
|
|
74
|
+
class="divide-default divide-y"
|
|
75
|
+
>
|
|
76
|
+
<li
|
|
77
|
+
v-for="item in addonGroup.addons"
|
|
78
|
+
:key="item.id"
|
|
79
|
+
class="flex items-center justify-between gap-2 px-3 py-2 text-sm"
|
|
80
|
+
>
|
|
81
|
+
<div>
|
|
82
|
+
{{ tr(item.label) }}
|
|
83
|
+
<span class="text-muted">· {{ item.price === 0 ? t("admin.included", "Included") : `+ ${formatPrice(item.price, ticketType.currency, locale)}` }}<template v-if="item.capacity !== null"> · {{ t("admin.capacity", "capacity") }} {{ item.capacity }}</template></span>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="flex shrink-0 gap-1">
|
|
86
|
+
<UButton
|
|
87
|
+
icon="i-lucide-pencil"
|
|
88
|
+
color="neutral"
|
|
89
|
+
variant="ghost"
|
|
90
|
+
size="xs"
|
|
91
|
+
:aria-label="t('admin.edit', 'Edit')"
|
|
92
|
+
@click="emit('editAddon', addonGroup, item)"
|
|
93
|
+
/>
|
|
94
|
+
<UButton
|
|
95
|
+
icon="i-lucide-trash"
|
|
96
|
+
color="error"
|
|
97
|
+
variant="ghost"
|
|
98
|
+
size="xs"
|
|
99
|
+
:aria-label="t('admin.remove', 'Delete')"
|
|
100
|
+
@click="removeAddon(item)"
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
</li>
|
|
104
|
+
</ul>
|
|
105
|
+
<p
|
|
106
|
+
v-else
|
|
107
|
+
class="text-muted px-3 py-2 text-xs"
|
|
108
|
+
>
|
|
109
|
+
{{ t("admin.noAddons", "No choices yet: buyers will see nothing to pick.") }}
|
|
110
|
+
</p>
|
|
111
|
+
</div>
|
|
112
|
+
<div>
|
|
113
|
+
<UButton
|
|
114
|
+
icon="i-lucide-plus"
|
|
115
|
+
color="neutral"
|
|
116
|
+
variant="outline"
|
|
117
|
+
size="xs"
|
|
118
|
+
:label="t('admin.addGroup', 'Add add-on group')"
|
|
119
|
+
@click="emit('addGroup')"
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type AdminAddon, type AdminAddonGroup, type AdminTicketType } from '../utils/admin-event.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
ticketType: AdminTicketType;
|
|
4
|
+
};
|
|
5
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
6
|
+
refresh: () => any;
|
|
7
|
+
addGroup: () => any;
|
|
8
|
+
editGroup: (group: AdminAddonGroup) => any;
|
|
9
|
+
addAddon: (group: AdminAddonGroup) => any;
|
|
10
|
+
editAddon: (group: AdminAddonGroup, addon: AdminAddon) => any;
|
|
11
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
12
|
+
onRefresh?: (() => any) | undefined;
|
|
13
|
+
onAddGroup?: (() => any) | undefined;
|
|
14
|
+
onEditGroup?: ((group: AdminAddonGroup) => any) | undefined;
|
|
15
|
+
onAddAddon?: ((group: AdminAddonGroup) => any) | undefined;
|
|
16
|
+
onEditAddon?: ((group: AdminAddonGroup, addon: AdminAddon) => any) | undefined;
|
|
17
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
18
|
+
declare const _default: typeof __VLS_export;
|
|
19
|
+
export default _default;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type AdminTicketType } from '../utils/admin-event.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
eventId: string;
|
|
4
|
+
/** Membership rules are only offered when @frigear-nu/subscriptions is installed. */
|
|
5
|
+
membershipEnabled: boolean;
|
|
6
|
+
plans: Array<{
|
|
7
|
+
label: string;
|
|
8
|
+
value: string;
|
|
9
|
+
}>;
|
|
10
|
+
};
|
|
11
|
+
declare function open(ticketType?: AdminTicketType): void;
|
|
12
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
13
|
+
open: typeof open;
|
|
14
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
15
|
+
refresh: () => any;
|
|
16
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
17
|
+
onRefresh?: (() => any) | undefined;
|
|
18
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
declare const _default: typeof __VLS_export;
|
|
20
|
+
export default _default;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { toCents, fromCents } from "@frigear-nu/ui/money";
|
|
3
|
+
import { DEFAULT_CURRENCY, ticketMdcData } from "../../utils";
|
|
4
|
+
import { useAdminAction } from "../utils/admin-event";
|
|
5
|
+
defineOptions({ name: "EventAdminTicketTypeDialog" });
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
eventId: { type: String, required: true },
|
|
8
|
+
membershipEnabled: { type: Boolean, required: true },
|
|
9
|
+
plans: { type: Array, required: true }
|
|
10
|
+
});
|
|
11
|
+
const emit = defineEmits(["refresh"]);
|
|
12
|
+
const t = useModuleT("events");
|
|
13
|
+
const locale = useEventsLocale();
|
|
14
|
+
const tr = useEventsTranslate();
|
|
15
|
+
const { busy, run } = useAdminAction();
|
|
16
|
+
const modes = computed(() => [
|
|
17
|
+
{ label: t("admin.ruleNone", "Nobody"), value: "none" },
|
|
18
|
+
{ label: t("admin.ruleAny", "Any membership"), value: "any" },
|
|
19
|
+
{ label: t("admin.rulePlans", "Specific memberships"), value: "plans" }
|
|
20
|
+
]);
|
|
21
|
+
const toRule = (value) => value === "any" ? { mode: "any", plans: [] } : Array.isArray(value) && value.length ? { mode: "plans", plans: [...value] } : { mode: "none", plans: [] };
|
|
22
|
+
const fromRule = (rule) => rule.mode === "any" ? "any" : rule.mode === "plans" && rule.plans.length ? [...rule.plans] : null;
|
|
23
|
+
const dialog = ref(false);
|
|
24
|
+
const editingType = ref(null);
|
|
25
|
+
const type = reactive({ key: "", name: void 0, description: void 0, price: 0, capacity: "", requires: toRule(null), excludes: toRule(null) });
|
|
26
|
+
function open(ticketType) {
|
|
27
|
+
editingType.value = ticketType ?? null;
|
|
28
|
+
Object.assign(type, {
|
|
29
|
+
key: ticketType?.key ?? "",
|
|
30
|
+
name: ticketType?.name,
|
|
31
|
+
description: ticketType?.description ?? void 0,
|
|
32
|
+
price: ticketType ? fromCents(ticketType.price, ticketType.currency) : 0,
|
|
33
|
+
capacity: ticketType?.capacity ?? "",
|
|
34
|
+
requires: toRule(ticketType?.requiresMembership),
|
|
35
|
+
excludes: toRule(ticketType?.excludesMembership)
|
|
36
|
+
});
|
|
37
|
+
dialog.value = true;
|
|
38
|
+
}
|
|
39
|
+
defineExpose({ open });
|
|
40
|
+
const save = () => run(async () => {
|
|
41
|
+
const body = {
|
|
42
|
+
name: type.name,
|
|
43
|
+
description: type.description ?? null,
|
|
44
|
+
// Prices are stored in minor units (øre/cents).
|
|
45
|
+
price: toCents(Number(type.price), editingType.value?.currency ?? DEFAULT_CURRENCY),
|
|
46
|
+
capacity: type.capacity === "" ? null : Number(type.capacity),
|
|
47
|
+
...props.membershipEnabled ? { requiresMembership: fromRule(type.requires), excludesMembership: fromRule(type.excludes) } : {}
|
|
48
|
+
};
|
|
49
|
+
if (editingType.value) {
|
|
50
|
+
await $fetch(`/api/events/admin/ticket-types/${editingType.value.id}`, { method: "PATCH", body });
|
|
51
|
+
} else {
|
|
52
|
+
await $fetch(`/api/events/admin/events/${props.eventId}/ticket-types`, { method: "POST", body: { key: type.key, ...body } });
|
|
53
|
+
}
|
|
54
|
+
dialog.value = false;
|
|
55
|
+
emit("refresh");
|
|
56
|
+
}, t("admin.typeSaved", "Ticket type saved."));
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<template>
|
|
60
|
+
<UModal
|
|
61
|
+
v-model:open="dialog"
|
|
62
|
+
:title="editingType ? t('admin.editType', 'Edit ticket type') : t('admin.addType', 'Add ticket type')"
|
|
63
|
+
>
|
|
64
|
+
<template #body>
|
|
65
|
+
<form
|
|
66
|
+
id="admin-ticket-type-form"
|
|
67
|
+
class="flex flex-col gap-4"
|
|
68
|
+
@submit.prevent="save"
|
|
69
|
+
>
|
|
70
|
+
<UFormField :label="t('admin.name', 'Name')">
|
|
71
|
+
<TranslatedInput
|
|
72
|
+
v-model="type.name"
|
|
73
|
+
required
|
|
74
|
+
/>
|
|
75
|
+
</UFormField>
|
|
76
|
+
<UFormField :label="t('admin.description', 'Description')">
|
|
77
|
+
<TranslatedInput
|
|
78
|
+
v-model="type.description"
|
|
79
|
+
multiline
|
|
80
|
+
mdc
|
|
81
|
+
:mdc-data="ticketMdcData({ ...type, price: toCents(Number(type.price), editingType?.currency ?? DEFAULT_CURRENCY), currency: editingType?.currency ?? DEFAULT_CURRENCY }, tr(type.name), locale)"
|
|
82
|
+
:rows="2"
|
|
83
|
+
/>
|
|
84
|
+
</UFormField>
|
|
85
|
+
<UFormField
|
|
86
|
+
v-if="!editingType"
|
|
87
|
+
:label="t('admin.key', 'Key')"
|
|
88
|
+
:description="t('admin.keyHelp', 'A short unique id, e.g. member.')"
|
|
89
|
+
>
|
|
90
|
+
<UInput
|
|
91
|
+
v-model="type.key"
|
|
92
|
+
class="w-full"
|
|
93
|
+
required
|
|
94
|
+
/>
|
|
95
|
+
</UFormField>
|
|
96
|
+
<UFormField :label="t('admin.price', 'Price')">
|
|
97
|
+
<UInput
|
|
98
|
+
v-model="type.price"
|
|
99
|
+
type="number"
|
|
100
|
+
min="0"
|
|
101
|
+
step="0.01"
|
|
102
|
+
class="w-full"
|
|
103
|
+
/>
|
|
104
|
+
</UFormField>
|
|
105
|
+
<UFormField
|
|
106
|
+
:label="t('admin.capacity', 'Capacity')"
|
|
107
|
+
:description="t('admin.capacityHelp', 'Leave empty for unlimited.')"
|
|
108
|
+
>
|
|
109
|
+
<UInput
|
|
110
|
+
v-model="type.capacity"
|
|
111
|
+
type="number"
|
|
112
|
+
min="0"
|
|
113
|
+
class="w-full"
|
|
114
|
+
/>
|
|
115
|
+
</UFormField>
|
|
116
|
+
|
|
117
|
+
<template v-if="membershipEnabled">
|
|
118
|
+
<UFormField
|
|
119
|
+
:label="t('admin.requiresMembership', 'Only for members of')"
|
|
120
|
+
:description="t('admin.requiresHelp', 'Visitors without a matching active membership cannot buy this ticket.')"
|
|
121
|
+
>
|
|
122
|
+
<USelect
|
|
123
|
+
v-model="type.requires.mode"
|
|
124
|
+
:items="modes"
|
|
125
|
+
class="w-full"
|
|
126
|
+
/>
|
|
127
|
+
<USelectMenu
|
|
128
|
+
v-if="type.requires.mode === 'plans'"
|
|
129
|
+
v-model="type.requires.plans"
|
|
130
|
+
:items="plans"
|
|
131
|
+
value-key="value"
|
|
132
|
+
multiple
|
|
133
|
+
class="mt-2 w-full"
|
|
134
|
+
/>
|
|
135
|
+
</UFormField>
|
|
136
|
+
<UFormField
|
|
137
|
+
:label="t('admin.excludesMembership', 'Not for members of')"
|
|
138
|
+
:description="t('admin.excludesHelp', 'Holders of these memberships cannot buy this ticket, so members and non-members can have separate prices.')"
|
|
139
|
+
>
|
|
140
|
+
<USelect
|
|
141
|
+
v-model="type.excludes.mode"
|
|
142
|
+
:items="modes"
|
|
143
|
+
class="w-full"
|
|
144
|
+
/>
|
|
145
|
+
<USelectMenu
|
|
146
|
+
v-if="type.excludes.mode === 'plans'"
|
|
147
|
+
v-model="type.excludes.plans"
|
|
148
|
+
:items="plans"
|
|
149
|
+
value-key="value"
|
|
150
|
+
multiple
|
|
151
|
+
class="mt-2 w-full"
|
|
152
|
+
/>
|
|
153
|
+
</UFormField>
|
|
154
|
+
</template>
|
|
155
|
+
</form>
|
|
156
|
+
</template>
|
|
157
|
+
<template #footer>
|
|
158
|
+
<div class="flex w-full justify-end gap-2">
|
|
159
|
+
<UButton
|
|
160
|
+
variant="subtle"
|
|
161
|
+
color="neutral"
|
|
162
|
+
:label="t('admin.cancel', 'Cancel')"
|
|
163
|
+
@click="dialog = false"
|
|
164
|
+
/>
|
|
165
|
+
<UButton
|
|
166
|
+
type="submit"
|
|
167
|
+
form="admin-ticket-type-form"
|
|
168
|
+
:loading="busy"
|
|
169
|
+
:disabled="!tr(type.name)"
|
|
170
|
+
:label="t('admin.save', 'Save')"
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
</template>
|
|
174
|
+
</UModal>
|
|
175
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type AdminTicketType } from '../utils/admin-event.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
eventId: string;
|
|
4
|
+
/** Membership rules are only offered when @frigear-nu/subscriptions is installed. */
|
|
5
|
+
membershipEnabled: boolean;
|
|
6
|
+
plans: Array<{
|
|
7
|
+
label: string;
|
|
8
|
+
value: string;
|
|
9
|
+
}>;
|
|
10
|
+
};
|
|
11
|
+
declare function open(ticketType?: AdminTicketType): void;
|
|
12
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
13
|
+
open: typeof open;
|
|
14
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
15
|
+
refresh: () => any;
|
|
16
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
17
|
+
onRefresh?: (() => any) | undefined;
|
|
18
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
declare const _default: typeof __VLS_export;
|
|
20
|
+
export default _default;
|
|
@@ -1,52 +1,64 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
|
|
3
|
-
const props = defineProps({
|
|
2
|
+
defineProps({
|
|
4
3
|
event: { type: Object, required: true },
|
|
5
4
|
to: { type: String, required: true }
|
|
6
5
|
});
|
|
7
6
|
const t = useModuleT("events");
|
|
8
7
|
const locale = useEventsLocale();
|
|
9
|
-
const
|
|
8
|
+
const tr = useEventsTranslate();
|
|
10
9
|
</script>
|
|
11
10
|
|
|
12
11
|
<template>
|
|
13
|
-
<
|
|
12
|
+
<NuxtLink
|
|
13
|
+
:to="to"
|
|
14
|
+
class="group flex flex-col overflow-hidden rounded-xl bg-default ring ring-default transition hover:shadow-lg"
|
|
15
|
+
>
|
|
14
16
|
<img
|
|
15
17
|
v-if="event.image"
|
|
16
18
|
:src="event.image"
|
|
17
|
-
:alt="
|
|
18
|
-
class="aspect-[
|
|
19
|
+
:alt="tr(event.name)"
|
|
20
|
+
class="aspect-[2/1] w-full object-cover"
|
|
19
21
|
>
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
· {{ event.location }}
|
|
26
|
-
</template>
|
|
27
|
-
</p>
|
|
28
|
-
<p
|
|
29
|
-
v-if="event.description"
|
|
30
|
-
class="text-sm"
|
|
31
|
-
>
|
|
32
|
-
{{ translate(event.description, locale) }}
|
|
33
|
-
</p>
|
|
34
|
-
<div class="flex items-center gap-2">
|
|
22
|
+
<div
|
|
23
|
+
v-else
|
|
24
|
+
class="gradient-linear-br-teal-purple aspect-[2/1] w-full"
|
|
25
|
+
/>
|
|
26
|
+
<div class="flex flex-1 flex-col gap-2 p-5">
|
|
35
27
|
<UBadge
|
|
36
28
|
v-if="event.requiresMembership"
|
|
37
29
|
color="warning"
|
|
38
30
|
variant="subtle"
|
|
39
31
|
icon="i-lucide-user-star"
|
|
32
|
+
class="self-start"
|
|
40
33
|
>
|
|
41
34
|
{{ t("members", "Members") }}
|
|
42
35
|
</UBadge>
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
<h3 class="text-lg font-semibold text-highlighted">
|
|
37
|
+
{{ tr(event.name) }}
|
|
38
|
+
</h3>
|
|
39
|
+
<p class="text-sm text-muted">
|
|
40
|
+
<NuxtTime
|
|
41
|
+
:datetime="event.startsAt"
|
|
42
|
+
:locale="locale"
|
|
43
|
+
date-style="full"
|
|
44
|
+
time-style="short"
|
|
45
|
+
/><template v-if="event.location">
|
|
46
|
+
· {{ tr(event.location) }}
|
|
47
|
+
</template>
|
|
48
|
+
</p>
|
|
49
|
+
<p
|
|
50
|
+
v-if="event.description"
|
|
51
|
+
class="line-clamp-3 text-sm text-muted"
|
|
47
52
|
>
|
|
53
|
+
{{ tr(event.description) }}
|
|
54
|
+
</p>
|
|
55
|
+
<span class="mt-auto inline-flex items-center gap-1 pt-2 text-sm font-medium text-primary">
|
|
48
56
|
{{ t("viewEvent", "View event") }}
|
|
49
|
-
|
|
57
|
+
<UIcon
|
|
58
|
+
name="i-lucide-arrow-right"
|
|
59
|
+
class="transition group-hover:translate-x-0.5"
|
|
60
|
+
/>
|
|
61
|
+
</span>
|
|
50
62
|
</div>
|
|
51
|
-
</
|
|
63
|
+
</NuxtLink>
|
|
52
64
|
</template>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { renderSVG } from "uqr";
|
|
3
|
-
import { formatPrice
|
|
3
|
+
import { formatMoney as formatPrice } from "@frigear-nu/ui/money";
|
|
4
4
|
const props = defineProps({
|
|
5
5
|
ticket: { type: Object, required: true },
|
|
6
6
|
to: { type: String, required: false }
|
|
@@ -8,6 +8,7 @@ const props = defineProps({
|
|
|
8
8
|
const emit = defineEmits(["pay"]);
|
|
9
9
|
const t = useModuleT("events");
|
|
10
10
|
const locale = useEventsLocale();
|
|
11
|
+
const tr = useEventsTranslate();
|
|
11
12
|
const colors = { paid: "success", pending: "warning", cancelled: "neutral", expired: "neutral", refunded: "error" };
|
|
12
13
|
const qr = computed(() => props.ticket.code ? `data:image/svg+xml;utf8,${encodeURIComponent(renderSVG(props.ticket.code, { border: 1 }))}` : null);
|
|
13
14
|
const showQr = ref(false);
|
|
@@ -18,14 +19,14 @@ const showQr = ref(false);
|
|
|
18
19
|
<div class="flex items-start gap-2">
|
|
19
20
|
<div>
|
|
20
21
|
<h3 class="font-semibold">
|
|
21
|
-
{{
|
|
22
|
+
{{ tr(ticket.eventName) }}
|
|
22
23
|
</h3>
|
|
23
24
|
<p class="text-sm text-muted">
|
|
24
|
-
{{
|
|
25
|
+
{{ tr(ticket.ticketTypeName) }}<template
|
|
25
26
|
v-for="addon in ticket.addons"
|
|
26
27
|
:key="addon.id"
|
|
27
28
|
>
|
|
28
|
-
· {{
|
|
29
|
+
· {{ tr(addon.label) }}
|
|
29
30
|
</template>
|
|
30
31
|
</p>
|
|
31
32
|
</div>
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import {
|
|
2
|
+
import { withMdcData } from "@frigear-nu/ui/markdown";
|
|
3
|
+
import { formatMoney as formatPrice } from "@frigear-nu/ui/money";
|
|
4
|
+
import { DEFAULT_CURRENCY, ticketMdcData } from "../../utils";
|
|
3
5
|
const props = defineProps({
|
|
4
6
|
ticketTypes: { type: Array, required: true },
|
|
5
7
|
loggedIn: { type: Boolean, required: true },
|
|
6
8
|
loading: { type: Boolean, required: false }
|
|
7
9
|
});
|
|
8
10
|
const emit = defineEmits(["purchase"]);
|
|
11
|
+
const t = useModuleT("events");
|
|
9
12
|
const locale = useEventsLocale();
|
|
13
|
+
const hasMdc = typeof resolveComponent("MDC") !== "string";
|
|
14
|
+
const tr = useEventsTranslate();
|
|
10
15
|
const selectedId = ref();
|
|
11
16
|
const chosen = ref({});
|
|
12
17
|
const selected = computed(() => props.ticketTypes.find((type) => type.id === selectedId.value));
|
|
@@ -22,11 +27,12 @@ function isAvailable(type) {
|
|
|
22
27
|
return type.saleState === "open" && !type.blocked;
|
|
23
28
|
}
|
|
24
29
|
const reason = (type) => {
|
|
25
|
-
if (type.saleState === "sold_out") return "Sold out";
|
|
26
|
-
if (type.saleState === "not_started") return "Sales not started";
|
|
27
|
-
if (type.saleState === "ended") return "Sales ended";
|
|
28
|
-
if (type.blocked === "membership_required") return "Members only";
|
|
29
|
-
if (type.blocked === "
|
|
30
|
+
if (type.saleState === "sold_out") return t("picker.soldOut", "Sold out");
|
|
31
|
+
if (type.saleState === "not_started") return t("picker.notStarted", "Sales not started");
|
|
32
|
+
if (type.saleState === "ended") return t("picker.ended", "Sales ended");
|
|
33
|
+
if (type.blocked === "membership_required") return t("picker.membersOnly", "Members only");
|
|
34
|
+
if (type.blocked === "membership_excluded") return t("picker.notForMembers", "Not for members");
|
|
35
|
+
if (type.blocked === "limit_reached") return t("picker.booked", "Already booked");
|
|
30
36
|
return null;
|
|
31
37
|
};
|
|
32
38
|
const addonIds = computed(() => Object.values(chosen.value).flat());
|
|
@@ -43,17 +49,23 @@ function toggle(groupId, addonId, multiple) {
|
|
|
43
49
|
|
|
44
50
|
<template>
|
|
45
51
|
<div class="flex flex-col gap-4">
|
|
52
|
+
<p
|
|
53
|
+
v-if="!ticketTypes.length"
|
|
54
|
+
class="text-sm text-muted"
|
|
55
|
+
>
|
|
56
|
+
{{ t("picker.noTickets", "No tickets are available for this event.") }}
|
|
57
|
+
</p>
|
|
46
58
|
<button
|
|
47
59
|
v-for="type in ticketTypes"
|
|
48
60
|
:key="type.id"
|
|
49
61
|
type="button"
|
|
50
|
-
class="rounded-
|
|
62
|
+
class="rounded-xl border p-4 text-left transition hover:border-primary/60 disabled:cursor-not-allowed disabled:opacity-60"
|
|
51
63
|
:class="selectedId === type.id ? 'border-primary ring-1 ring-primary' : 'border-default'"
|
|
52
64
|
:disabled="!isAvailable(type)"
|
|
53
65
|
@click="selectedId = type.id"
|
|
54
66
|
>
|
|
55
67
|
<div class="flex items-center gap-2">
|
|
56
|
-
<span class="font-medium">{{
|
|
68
|
+
<span class="font-medium">{{ tr(type.name) }}</span>
|
|
57
69
|
<UBadge
|
|
58
70
|
v-if="reason(type)"
|
|
59
71
|
color="neutral"
|
|
@@ -66,19 +78,26 @@ function toggle(groupId, addonId, multiple) {
|
|
|
66
78
|
color="warning"
|
|
67
79
|
variant="subtle"
|
|
68
80
|
>
|
|
69
|
-
{{ type.remaining }} left
|
|
81
|
+
{{ type.remaining }} {{ t("picker.left", "left") }}
|
|
70
82
|
</UBadge>
|
|
71
83
|
<span
|
|
72
84
|
v-if="!type.hidePrice"
|
|
73
|
-
class="ml-auto"
|
|
74
|
-
>{{ type.price === 0 ? "Free" : formatPrice(type.price, type.currency, locale) }}</span>
|
|
85
|
+
class="ml-auto font-bold text-primary"
|
|
86
|
+
>{{ type.price === 0 ? t("picker.free", "Free") : formatPrice(type.price, type.currency, locale) }}</span>
|
|
75
87
|
</div>
|
|
76
|
-
<
|
|
88
|
+
<div
|
|
77
89
|
v-if="type.description"
|
|
78
90
|
class="mt-1 text-sm text-muted"
|
|
79
91
|
>
|
|
80
|
-
|
|
81
|
-
|
|
92
|
+
<MDC
|
|
93
|
+
v-if="hasMdc"
|
|
94
|
+
:value="withMdcData(tr(type.description), ticketMdcData(type, tr(type.name), locale))"
|
|
95
|
+
class="prose prose-sm max-w-none text-muted"
|
|
96
|
+
/>
|
|
97
|
+
<template v-else>
|
|
98
|
+
{{ tr(type.description) }}
|
|
99
|
+
</template>
|
|
100
|
+
</div>
|
|
82
101
|
</button>
|
|
83
102
|
|
|
84
103
|
<fieldset
|
|
@@ -87,7 +106,7 @@ function toggle(groupId, addonId, multiple) {
|
|
|
87
106
|
class="flex flex-col gap-2"
|
|
88
107
|
>
|
|
89
108
|
<legend class="mb-1 font-medium">
|
|
90
|
-
{{
|
|
109
|
+
{{ tr(group.title) }}<span
|
|
91
110
|
v-if="group.required"
|
|
92
111
|
class="text-error"
|
|
93
112
|
> *</span>
|
|
@@ -96,20 +115,20 @@ function toggle(groupId, addonId, multiple) {
|
|
|
96
115
|
v-for="addon in group.addons"
|
|
97
116
|
:key="addon.id"
|
|
98
117
|
type="button"
|
|
99
|
-
class="flex items-center gap-2 rounded-
|
|
118
|
+
class="flex items-center gap-2 rounded-lg border p-3 text-left disabled:opacity-60"
|
|
100
119
|
:class="chosen[group.id]?.includes(addon.id) ? 'border-primary ring-1 ring-primary' : 'border-default'"
|
|
101
120
|
:disabled="addon.soldOut"
|
|
102
121
|
@click="toggle(group.id, addon.id, group.multiple)"
|
|
103
122
|
>
|
|
104
|
-
<span>{{
|
|
123
|
+
<span>{{ tr(addon.label) }}</span>
|
|
105
124
|
<UBadge
|
|
106
125
|
v-if="addon.soldOut"
|
|
107
126
|
color="neutral"
|
|
108
127
|
variant="subtle"
|
|
109
128
|
>
|
|
110
|
-
Sold out
|
|
129
|
+
{{ t("picker.soldOut", "Sold out") }}
|
|
111
130
|
</UBadge>
|
|
112
|
-
<span class="ml-auto text-sm">{{ addon.price === 0 ? "Included" : `+ ${formatPrice(addon.price, selected.currency, locale)}` }}</span>
|
|
131
|
+
<span class="ml-auto text-sm">{{ addon.price === 0 ? t("picker.included", "Included") : `+ ${formatPrice(addon.price, selected.currency, locale)}` }}</span>
|
|
113
132
|
</button>
|
|
114
133
|
</fieldset>
|
|
115
134
|
|
|
@@ -121,13 +140,13 @@ function toggle(groupId, addonId, multiple) {
|
|
|
121
140
|
:disabled="!selected || !isAvailable(selected) || missingRequired"
|
|
122
141
|
@click="emit('purchase', selected.id, addonIds)"
|
|
123
142
|
>
|
|
124
|
-
{{ total === 0 ? "Get free ticket" :
|
|
143
|
+
{{ total === 0 ? t("picker.getFree", "Get free ticket") : `${t("picker.buy", "Buy ticket")} \xB7 ${formatPrice(total, selected?.currency ?? DEFAULT_CURRENCY, locale)}` }}
|
|
125
144
|
</UButton>
|
|
126
145
|
<UAlert
|
|
127
146
|
v-else
|
|
128
147
|
color="neutral"
|
|
129
148
|
variant="subtle"
|
|
130
|
-
title="Sign in to get a ticket."
|
|
149
|
+
:title="t('picker.signIn', 'Sign in to get a ticket.')"
|
|
131
150
|
/>
|
|
132
151
|
</div>
|
|
133
152
|
</template>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { useEvents, useEvent, useMyTickets, useEventsLocale } from './useEvents.js';
|
|
1
|
+
export { useEvents, useEvent, useMyTickets, useEventsLocale, useEventsFallbackLocale, useEventsTranslate } from './useEvents.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { useEvents, useEvent, useMyTickets, useEventsLocale } from "./useEvents.js";
|
|
1
|
+
export { useEvents, useEvent, useMyTickets, useEventsLocale, useEventsFallbackLocale, useEventsTranslate } from "./useEvents.js";
|