@frigear-nu/events 0.0.7 → 0.0.9

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 (114) 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 +59 -41
  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/app/widgets/UpcomingEvents.d.vue.ts +3 -0
  47. package/dist/runtime/app/widgets/UpcomingEvents.vue +39 -0
  48. package/dist/runtime/app/widgets/UpcomingEvents.vue.d.ts +3 -0
  49. package/dist/runtime/app/widgets/UpcomingEventsStat.d.vue.ts +3 -0
  50. package/dist/runtime/app/widgets/UpcomingEventsStat.vue +17 -0
  51. package/dist/runtime/app/widgets/UpcomingEventsStat.vue.d.ts +3 -0
  52. package/dist/runtime/i18n/lang/da.json +33 -0
  53. package/dist/runtime/i18n/lang/en.json +33 -0
  54. package/dist/runtime/menus.js +15 -1
  55. package/dist/runtime/server/db/schema.sqlite.d.ts +62 -5
  56. package/dist/runtime/server/db/schema.sqlite.js +27 -2
  57. package/dist/runtime/server/mcp/money.d.ts +24 -0
  58. package/dist/runtime/server/mcp/money.js +42 -0
  59. package/dist/runtime/server/mcp/schemas.d.ts +119 -0
  60. package/dist/runtime/server/mcp/schemas.js +59 -0
  61. package/dist/runtime/server/mcp/tools/admin/create-event.d.ts +2 -0
  62. package/dist/runtime/server/mcp/tools/admin/create-event.js +12 -0
  63. package/dist/runtime/server/mcp/tools/admin/get-event-definition.d.ts +2 -0
  64. package/dist/runtime/server/mcp/tools/admin/get-event-definition.js +11 -0
  65. package/dist/runtime/server/mcp/tools/admin/list-all-events.d.ts +2 -0
  66. package/dist/runtime/server/mcp/tools/admin/list-all-events.js +9 -0
  67. package/dist/runtime/server/mcp/tools/admin/list-attendees.d.ts +2 -0
  68. package/dist/runtime/server/mcp/tools/admin/list-attendees.js +26 -0
  69. package/dist/runtime/server/mcp/tools/admin/update-event.d.ts +2 -0
  70. package/dist/runtime/server/mcp/tools/admin/update-event.js +10 -0
  71. package/dist/runtime/server/mcp/tools/admin/update-ticket-type.d.ts +2 -0
  72. package/dist/runtime/server/mcp/tools/admin/update-ticket-type.js +17 -0
  73. package/dist/runtime/server/mcp/tools/get-event.d.ts +2 -0
  74. package/dist/runtime/server/mcp/tools/get-event.js +13 -0
  75. package/dist/runtime/server/mcp/tools/list-events.d.ts +2 -0
  76. package/dist/runtime/server/mcp/tools/list-events.js +6 -0
  77. package/dist/runtime/server/mcp/tools/my-tickets.d.ts +2 -0
  78. package/dist/runtime/server/mcp/tools/my-tickets.js +7 -0
  79. package/dist/runtime/server/plugins/staff-scopes.d.ts +2 -0
  80. package/dist/runtime/server/plugins/staff-scopes.js +1 -0
  81. package/dist/runtime/server/plugins/subscriptions-membership.d.ts +2 -0
  82. package/dist/runtime/server/plugins/subscriptions-membership.js +11 -0
  83. package/dist/runtime/server/routes/admin/event-clone.post.d.ts +2 -0
  84. package/dist/runtime/server/routes/admin/event-clone.post.js +31 -0
  85. package/dist/runtime/server/routes/admin/event.get.d.ts +2 -0
  86. package/dist/runtime/server/routes/admin/event.get.js +5 -0
  87. package/dist/runtime/server/routes/admin/event.patch.js +3 -8
  88. package/dist/runtime/server/routes/admin/events.post.js +3 -13
  89. package/dist/runtime/server/routes/admin/membership-options.get.d.ts +2 -0
  90. package/dist/runtime/server/routes/admin/membership-options.get.js +4 -0
  91. package/dist/runtime/server/routes/admin/ticket-type.patch.js +3 -8
  92. package/dist/runtime/server/routes/admin/ticket-types.post.js +2 -1
  93. package/dist/runtime/server/routes/admin/upcoming.get.d.ts +2 -0
  94. package/dist/runtime/server/routes/admin/upcoming.get.js +17 -0
  95. package/dist/runtime/server/routes/public/event.get.js +2 -2
  96. package/dist/runtime/server/routes/public/purchase.post.js +3 -2
  97. package/dist/runtime/server/routes/public/ticket-pay.post.js +1 -1
  98. package/dist/runtime/server/routes/public/ticket.get.js +1 -1
  99. package/dist/runtime/server/routes/public/tickets-me.get.js +1 -1
  100. package/dist/runtime/server/utils/admin-schemas.d.ts +102 -20
  101. package/dist/runtime/server/utils/admin-schemas.js +8 -2
  102. package/dist/runtime/server/utils/admin.d.ts +82 -1
  103. package/dist/runtime/server/utils/admin.js +51 -1
  104. package/dist/runtime/server/utils/auth.d.ts +7 -6
  105. package/dist/runtime/server/utils/auth.js +4 -4
  106. package/dist/runtime/server/utils/catalog.d.ts +14 -0
  107. package/dist/runtime/server/utils/catalog.js +20 -4
  108. package/dist/runtime/server/utils/membership.d.ts +6 -2
  109. package/dist/runtime/server/utils/membership.js +15 -2
  110. package/dist/runtime/server/utils/tickets.js +15 -12
  111. package/dist/runtime/utils/index.d.ts +17 -7
  112. package/dist/runtime/utils/index.js +16 -19
  113. package/dist/types.d.mts +3 -1
  114. package/package.json +20 -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
- import { translate } from "../../utils";
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 date = computed(() => new Intl.DateTimeFormat(locale.value, { dateStyle: "full", timeStyle: "short" }).format(new Date(props.event.startsAt)));
8
+ const tr = useEventsTranslate();
10
9
  </script>
11
10
 
12
11
  <template>
13
- <UCard :ui="{ body: 'flex flex-col gap-2' }">
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="translate(event.name, locale)"
18
- class="aspect-[3/1] w-full rounded-md object-cover"
19
+ :alt="tr(event.name)"
20
+ class="aspect-[2/1] w-full object-cover"
19
21
  >
20
- <h3 class="text-lg font-semibold">
21
- {{ translate(event.name, locale) }}
22
- </h3>
23
- <p class="text-sm text-muted">
24
- {{ date }}<template v-if="event.location">
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
- <UButton
44
- :to="to"
45
- class="ml-auto"
46
- trailing-icon="i-lucide-arrow-right"
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
- </UButton>
57
+ <UIcon
58
+ name="i-lucide-arrow-right"
59
+ class="transition group-hover:translate-x-0.5"
60
+ />
61
+ </span>
50
62
  </div>
51
- </UCard>
63
+ </NuxtLink>
52
64
  </template>
@@ -1,6 +1,6 @@
1
1
  <script setup>
2
2
  import { renderSVG } from "uqr";
3
- import { formatPrice, translate } from "../../utils";
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
- {{ translate(ticket.eventName, locale) }}
22
+ {{ tr(ticket.eventName) }}
22
23
  </h3>
23
24
  <p class="text-sm text-muted">
24
- {{ translate(ticket.ticketTypeName, locale) }}<template
25
+ {{ tr(ticket.ticketTypeName) }}<template
25
26
  v-for="addon in ticket.addons"
26
27
  :key="addon.id"
27
28
  >
28
- · {{ translate(addon.label, locale) }}
29
+ · {{ tr(addon.label) }}
29
30
  </template>
30
31
  </p>
31
32
  </div>
@@ -1,12 +1,17 @@
1
1
  <script setup>
2
- import { formatPrice, translate } from "../../utils";
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 === "limit_reached") return "Already booked";
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-lg border p-4 text-left transition disabled:cursor-not-allowed disabled:opacity-60"
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">{{ translate(type.name, locale) }}</span>
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
- <p
88
+ <div
77
89
  v-if="type.description"
78
90
  class="mt-1 text-sm text-muted"
79
91
  >
80
- {{ translate(type.description, locale) }}
81
- </p>
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
- {{ translate(group.title, locale) }}<span
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-md border p-3 text-left disabled:opacity-60"
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>{{ translate(addon.label, locale) }}</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" : `Buy ticket \xB7 ${formatPrice(total, selected?.currency ?? "DKK", locale)}` }}
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";