@frigear-nu/events 0.0.0-stage → 0.0.2
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 +35 -2
- package/dist/module.d.mts +168 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +113 -0
- package/dist/runtime/app/components/EventCard.d.vue.ts +8 -0
- package/dist/runtime/app/components/EventCard.vue +52 -0
- package/dist/runtime/app/components/EventCard.vue.d.ts +8 -0
- package/dist/runtime/app/components/EventTicketCard.d.vue.ts +12 -0
- package/dist/runtime/app/components/EventTicketCard.vue +82 -0
- package/dist/runtime/app/components/EventTicketCard.vue.d.ts +12 -0
- package/dist/runtime/app/components/EventTicketPicker.d.vue.ts +13 -0
- package/dist/runtime/app/components/EventTicketPicker.vue +133 -0
- package/dist/runtime/app/components/EventTicketPicker.vue.d.ts +13 -0
- package/dist/runtime/app/composables/index.d.ts +1 -0
- package/dist/runtime/app/composables/index.js +1 -0
- package/dist/runtime/app/composables/useEvents.d.ts +21 -0
- package/dist/runtime/app/composables/useEvents.js +50 -0
- package/dist/runtime/app/pages/event.d.vue.ts +3 -0
- package/dist/runtime/app/pages/event.vue +135 -0
- package/dist/runtime/app/pages/event.vue.d.ts +3 -0
- package/dist/runtime/app/pages/events.d.vue.ts +3 -0
- package/dist/runtime/app/pages/events.vue +43 -0
- package/dist/runtime/app/pages/events.vue.d.ts +3 -0
- package/dist/runtime/app/pages/tickets.d.vue.ts +3 -0
- package/dist/runtime/app/pages/tickets.vue +30 -0
- package/dist/runtime/app/pages/tickets.vue.d.ts +3 -0
- package/dist/runtime/i18n/lang/da.json +87 -0
- package/dist/runtime/i18n/lang/en.json +87 -0
- package/dist/runtime/menus.d.ts +2 -0
- package/dist/runtime/menus.js +14 -0
- package/dist/runtime/server/db/schema.sqlite.d.ts +1095 -0
- package/dist/runtime/server/db/schema.sqlite.js +100 -0
- package/dist/runtime/server/plugins/payment-hooks.d.ts +2 -0
- package/dist/runtime/server/plugins/payment-hooks.js +34 -0
- package/dist/runtime/server/routes/admin/addon-group.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/addon-group.delete.js +7 -0
- package/dist/runtime/server/routes/admin/addon-group.patch.d.ts +2 -0
- package/dist/runtime/server/routes/admin/addon-group.patch.js +11 -0
- package/dist/runtime/server/routes/admin/addon-groups.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/addon-groups.post.js +11 -0
- package/dist/runtime/server/routes/admin/addon.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/addon.delete.js +7 -0
- package/dist/runtime/server/routes/admin/addon.patch.d.ts +2 -0
- package/dist/runtime/server/routes/admin/addon.patch.js +10 -0
- package/dist/runtime/server/routes/admin/addons.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/addons.post.js +12 -0
- package/dist/runtime/server/routes/admin/attendees.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/attendees.get.js +11 -0
- package/dist/runtime/server/routes/admin/event.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/event.delete.js +10 -0
- package/dist/runtime/server/routes/admin/event.patch.d.ts +2 -0
- package/dist/runtime/server/routes/admin/event.patch.js +11 -0
- package/dist/runtime/server/routes/admin/events.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/events.get.js +6 -0
- package/dist/runtime/server/routes/admin/events.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/events.post.js +19 -0
- package/dist/runtime/server/routes/admin/ticket-cancel.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/ticket-cancel.post.js +14 -0
- package/dist/runtime/server/routes/admin/ticket-refund.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/ticket-refund.post.js +10 -0
- package/dist/runtime/server/routes/admin/ticket-type.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/ticket-type.delete.js +10 -0
- package/dist/runtime/server/routes/admin/ticket-type.patch.d.ts +2 -0
- package/dist/runtime/server/routes/admin/ticket-type.patch.js +11 -0
- package/dist/runtime/server/routes/admin/ticket-types.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/ticket-types.post.js +11 -0
- package/dist/runtime/server/routes/public/check-in.post.d.ts +2 -0
- package/dist/runtime/server/routes/public/check-in.post.js +15 -0
- package/dist/runtime/server/routes/public/event.get.d.ts +2 -0
- package/dist/runtime/server/routes/public/event.get.js +7 -0
- package/dist/runtime/server/routes/public/events.get.d.ts +2 -0
- package/dist/runtime/server/routes/public/events.get.js +1 -0
- package/dist/runtime/server/routes/public/purchase.post.d.ts +2 -0
- package/dist/runtime/server/routes/public/purchase.post.js +12 -0
- package/dist/runtime/server/routes/public/ticket-pay.post.d.ts +2 -0
- package/dist/runtime/server/routes/public/ticket-pay.post.js +10 -0
- package/dist/runtime/server/routes/public/ticket.get.d.ts +2 -0
- package/dist/runtime/server/routes/public/ticket.get.js +4 -0
- package/dist/runtime/server/routes/public/tickets-me.get.d.ts +2 -0
- package/dist/runtime/server/routes/public/tickets-me.get.js +4 -0
- package/dist/runtime/server/utils/admin-schemas.d.ts +96 -0
- package/dist/runtime/server/utils/admin-schemas.js +48 -0
- package/dist/runtime/server/utils/admin.d.ts +7 -0
- package/dist/runtime/server/utils/admin.js +24 -0
- package/dist/runtime/server/utils/auth.d.ts +10 -0
- package/dist/runtime/server/utils/auth.js +7 -0
- package/dist/runtime/server/utils/catalog.d.ts +25 -0
- package/dist/runtime/server/utils/catalog.js +129 -0
- package/dist/runtime/server/utils/membership.d.ts +3 -0
- package/dist/runtime/server/utils/membership.js +5 -0
- package/dist/runtime/server/utils/tickets.d.ts +10 -0
- package/dist/runtime/server/utils/tickets.js +122 -0
- package/dist/runtime/utils/index.d.ts +14 -0
- package/dist/runtime/utils/index.js +30 -0
- package/dist/types.d.mts +3 -0
- package/package.json +91 -3
package/README.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @frigear-nu/events
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Database-backed events for the frigear ecosystem: ticket types (free and paid), addons such as food, capacity, sale
|
|
4
|
+
windows, per-user limits, optional membership requirements, Stripe payments through
|
|
5
|
+
[`@frigear-nu/payments`](../payments), QR check-in and opt-in pages built with [`@nuxt/ui`](https://ui.nuxt.com).
|
|
6
|
+
|
|
7
|
+
## Quick Setup
|
|
8
|
+
|
|
9
|
+
Requires `@nuxthub/core` with `hub.kv: true`, `@frigear-nu/users`, `@frigear-nu/stripe` and `@frigear-nu/payments`.
|
|
10
|
+
`@frigear-nu/subscriptions` (membership requirements) and `@frigear-nu/menu` are optional.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
export default defineNuxtConfig({
|
|
14
|
+
modules: ['@frigear-nu/users', '@frigear-nu/stripe', '@frigear-nu/payments', '@frigear-nu/events'],
|
|
15
|
+
events: { settings: { pages: { events: true, event: true, tickets: true } } },
|
|
16
|
+
})
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Generate and commit a migration for the events tables in your app (see the repository `AGENTS.md`).
|
|
20
|
+
|
|
21
|
+
Staff routes (event management, check-in) are denied until the host implements the `events:authorize` Nitro hook:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
export default defineNitroPlugin((nitro) => {
|
|
25
|
+
nitro.hooks.hook('events:authorize', (ctx) => { ctx.allowed = isStaff(ctx.userId) })
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Full documentation: `apps/docs/content/4.modules/25.events.md`.
|
|
30
|
+
|
|
31
|
+
## Development
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pnpm --filter @frigear-nu/events test
|
|
35
|
+
pnpm --filter @frigear-nu/events lint
|
|
36
|
+
```
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
import { H3Event } from 'h3';
|
|
3
|
+
|
|
4
|
+
/** A text that is either one string or a map of locale -> string. */
|
|
5
|
+
type Translated = string | Record<string, string>;
|
|
6
|
+
/** `null` = open to everyone, `'any'` = any active subscription, or a list of plan keys. */
|
|
7
|
+
type MembershipRequirement = 'any' | string[] | null;
|
|
8
|
+
type EventStatus = 'draft' | 'published' | 'archived';
|
|
9
|
+
type TicketStatus = 'pending' | 'paid' | 'cancelled' | 'refunded' | 'expired';
|
|
10
|
+
type ModuleOptions = {
|
|
11
|
+
settings?: {
|
|
12
|
+
pages?: {
|
|
13
|
+
/** Event list. `true` = `/events`. */
|
|
14
|
+
events?: string | boolean;
|
|
15
|
+
/** Event detail; must contain a `:slug` param. `true` = `/events/:slug`. */
|
|
16
|
+
event?: string | boolean;
|
|
17
|
+
/** The signed-in user's tickets. `true` = `/account/tickets`. */
|
|
18
|
+
tickets?: string | boolean;
|
|
19
|
+
};
|
|
20
|
+
/** Minutes an unpaid ticket keeps its seat (the Stripe session expires then). Defaults to `30`. */
|
|
21
|
+
reservationMinutes?: number;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
type EventsRuntimeConfig = {
|
|
25
|
+
reservationMinutes: number;
|
|
26
|
+
/** Whether `@frigear-nu/subscriptions` is installed (membership requirements can be evaluated). */
|
|
27
|
+
membershipEnabled: boolean;
|
|
28
|
+
};
|
|
29
|
+
type PublicModuleOptions = {
|
|
30
|
+
pages: {
|
|
31
|
+
events?: string;
|
|
32
|
+
event?: string;
|
|
33
|
+
tickets?: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
type AddonDto = {
|
|
37
|
+
id: string;
|
|
38
|
+
label: Translated;
|
|
39
|
+
description: Translated | null;
|
|
40
|
+
price: number;
|
|
41
|
+
soldOut: boolean;
|
|
42
|
+
};
|
|
43
|
+
type AddonGroupDto = {
|
|
44
|
+
id: string;
|
|
45
|
+
title: Translated;
|
|
46
|
+
required: boolean;
|
|
47
|
+
multiple: boolean;
|
|
48
|
+
addons: AddonDto[];
|
|
49
|
+
};
|
|
50
|
+
type TicketTypeDto = {
|
|
51
|
+
id: string;
|
|
52
|
+
key: string;
|
|
53
|
+
name: Translated;
|
|
54
|
+
description: Translated | null;
|
|
55
|
+
price: number;
|
|
56
|
+
currency: string;
|
|
57
|
+
hidePrice: boolean;
|
|
58
|
+
isDefault: boolean;
|
|
59
|
+
requiresMembership: MembershipRequirement;
|
|
60
|
+
/** Seats left, or null when unlimited. */
|
|
61
|
+
remaining: number | null;
|
|
62
|
+
saleStartsAt: string | null;
|
|
63
|
+
saleEndsAt: string | null;
|
|
64
|
+
/** `open | not_started | ended | sold_out`. */
|
|
65
|
+
saleState: 'open' | 'not_started' | 'ended' | 'sold_out';
|
|
66
|
+
/** Only set for signed-in users: `null` when allowed, otherwise why not. */
|
|
67
|
+
blocked: 'membership_required' | 'limit_reached' | null;
|
|
68
|
+
addonGroups: AddonGroupDto[];
|
|
69
|
+
};
|
|
70
|
+
type EventSummaryDto = {
|
|
71
|
+
id: string;
|
|
72
|
+
slug: string;
|
|
73
|
+
name: Translated;
|
|
74
|
+
description: Translated | null;
|
|
75
|
+
image: string | null;
|
|
76
|
+
location: string | null;
|
|
77
|
+
startsAt: string;
|
|
78
|
+
endsAt: string | null;
|
|
79
|
+
requiresMembership: MembershipRequirement;
|
|
80
|
+
};
|
|
81
|
+
type EventDetailDto = EventSummaryDto & {
|
|
82
|
+
body: Translated | null;
|
|
83
|
+
locationUrl: string | null;
|
|
84
|
+
ticketTypes: TicketTypeDto[];
|
|
85
|
+
};
|
|
86
|
+
type PurchaseBody = {
|
|
87
|
+
ticketTypeId: string;
|
|
88
|
+
addonIds?: string[];
|
|
89
|
+
successUrl?: string;
|
|
90
|
+
cancelUrl?: string;
|
|
91
|
+
};
|
|
92
|
+
type PurchaseResponse = {
|
|
93
|
+
ticket: TicketDto;
|
|
94
|
+
/** Redirect target for paid tickets; absent when the ticket was free and is already `paid`. */
|
|
95
|
+
url?: string;
|
|
96
|
+
};
|
|
97
|
+
type TicketDto = {
|
|
98
|
+
id: string;
|
|
99
|
+
eventId: string;
|
|
100
|
+
eventSlug: string;
|
|
101
|
+
eventName: Translated;
|
|
102
|
+
startsAt: string;
|
|
103
|
+
ticketTypeId: string;
|
|
104
|
+
ticketTypeName: Translated;
|
|
105
|
+
status: TicketStatus;
|
|
106
|
+
addons: Array<{
|
|
107
|
+
id: string;
|
|
108
|
+
label: Translated;
|
|
109
|
+
price: number;
|
|
110
|
+
}>;
|
|
111
|
+
amount: number;
|
|
112
|
+
currency: string;
|
|
113
|
+
paymentId: string | null;
|
|
114
|
+
/** The QR payload; only included for paid tickets. */
|
|
115
|
+
code: string | null;
|
|
116
|
+
checkedInAt: string | null;
|
|
117
|
+
createdAt: string;
|
|
118
|
+
};
|
|
119
|
+
type CheckInResponse = {
|
|
120
|
+
ok: boolean;
|
|
121
|
+
alreadyCheckedIn: boolean;
|
|
122
|
+
checkedInAt: string;
|
|
123
|
+
ticket: TicketDto;
|
|
124
|
+
};
|
|
125
|
+
type PurchaseBeforeContext = {
|
|
126
|
+
event: H3Event;
|
|
127
|
+
userId: number;
|
|
128
|
+
eventId: string;
|
|
129
|
+
ticketTypeId: string;
|
|
130
|
+
addonIds: string[];
|
|
131
|
+
};
|
|
132
|
+
type AuthorizeContext = {
|
|
133
|
+
event: H3Event;
|
|
134
|
+
userId: number;
|
|
135
|
+
/** Set `ctx.allowed = true` to let the user manage events / check tickets in. Denied by default. */
|
|
136
|
+
allowed: boolean;
|
|
137
|
+
};
|
|
138
|
+
type TicketHookContext = {
|
|
139
|
+
ticket: TicketDto;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
declare module '@nuxt/schema' {
|
|
143
|
+
interface NuxtConfig {
|
|
144
|
+
events?: ModuleOptions;
|
|
145
|
+
}
|
|
146
|
+
interface RuntimeConfig {
|
|
147
|
+
events: EventsRuntimeConfig;
|
|
148
|
+
}
|
|
149
|
+
interface PublicRuntimeConfig {
|
|
150
|
+
events: PublicModuleOptions;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
declare module 'nitropack' {
|
|
154
|
+
interface NitroRuntimeHooks {
|
|
155
|
+
/** Staff gate (event management, check-in): set `ctx.allowed = true` to let the user in. Denied by default. */
|
|
156
|
+
'events:authorize': (ctx: AuthorizeContext) => void | Promise<void>;
|
|
157
|
+
/** Policy hook: throw to deny a purchase. */
|
|
158
|
+
'events:purchase:before': (ctx: PurchaseBeforeContext) => void | Promise<void>;
|
|
159
|
+
/** A ticket became `paid` (free tickets immediately, paid ones after the Stripe event). Send confirmations here. */
|
|
160
|
+
'events:ticket:paid': (ctx: TicketHookContext) => void | Promise<void>;
|
|
161
|
+
/** A paid ticket was fully refunded. */
|
|
162
|
+
'events:ticket:refunded': (ctx: TicketHookContext) => void | Promise<void>;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
166
|
+
|
|
167
|
+
export { _default as default };
|
|
168
|
+
export type { AddonDto, AddonGroupDto, AuthorizeContext, CheckInResponse, EventDetailDto, EventStatus, EventSummaryDto, EventsRuntimeConfig, MembershipRequirement, ModuleOptions, PublicModuleOptions, PurchaseBeforeContext, PurchaseBody, PurchaseResponse, TicketDto, TicketHookContext, TicketStatus, TicketTypeDto, Translated };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { defineNuxtModule, createResolver, hasNuxtModule, addServerTemplate, addTypeTemplate, addServerImportsDir, addServerPlugin, addImports, addComponentsDir, extendPages, addServerHandler } from '@nuxt/kit';
|
|
2
|
+
import { registerModule, registerLocales, registerDatabaseTable, defineModulePage } from '@frigear-nu/module-kit/utils';
|
|
3
|
+
|
|
4
|
+
const MODULE_NAME = "@frigear-nu/events";
|
|
5
|
+
function resolvePagePath(setting, defaultPath) {
|
|
6
|
+
if (typeof setting === "string") return setting;
|
|
7
|
+
if (setting === true) return defaultPath;
|
|
8
|
+
return void 0;
|
|
9
|
+
}
|
|
10
|
+
const module$1 = defineNuxtModule({
|
|
11
|
+
meta: { name: MODULE_NAME, configKey: "events" },
|
|
12
|
+
defaults: {
|
|
13
|
+
settings: { pages: {}, reservationMinutes: 30 }
|
|
14
|
+
},
|
|
15
|
+
moduleDependencies: {
|
|
16
|
+
"@nuxthub/core": {
|
|
17
|
+
version: ">=0.10",
|
|
18
|
+
defaults: { kv: true }
|
|
19
|
+
},
|
|
20
|
+
"@frigear-nu/module-kit": { version: ">=0.0.1" },
|
|
21
|
+
"@frigear-nu/users": { version: ">=0.0.1" },
|
|
22
|
+
"@frigear-nu/stripe": { version: ">=0.0.1" },
|
|
23
|
+
"@frigear-nu/payments": { version: ">=0.0.1" },
|
|
24
|
+
"@frigear-nu/subscriptions": { version: ">=0.0.1", optional: true },
|
|
25
|
+
"@frigear-nu/menu": { version: ">=0.0.1", optional: true }
|
|
26
|
+
},
|
|
27
|
+
async setup(options, nuxt) {
|
|
28
|
+
registerModule(MODULE_NAME, nuxt);
|
|
29
|
+
const resolver = createResolver(import.meta.url);
|
|
30
|
+
registerLocales(nuxt, resolver.resolve("./runtime/i18n/lang"));
|
|
31
|
+
const settings = options.settings ?? {};
|
|
32
|
+
const withMenus = hasNuxtModule("@frigear-nu/menu");
|
|
33
|
+
const withMembership = hasNuxtModule("@frigear-nu/subscriptions");
|
|
34
|
+
const eventsPath = resolvePagePath(settings.pages?.events, "/events");
|
|
35
|
+
const eventPath = resolvePagePath(settings.pages?.event, "/events/:slug");
|
|
36
|
+
const ticketsPath = resolvePagePath(settings.pages?.tickets, "/account/tickets");
|
|
37
|
+
nuxt.options.runtimeConfig.events = {
|
|
38
|
+
reservationMinutes: settings.reservationMinutes ?? 30,
|
|
39
|
+
membershipEnabled: withMembership
|
|
40
|
+
};
|
|
41
|
+
nuxt.options.runtimeConfig.public.events = {
|
|
42
|
+
pages: { events: eventsPath, event: eventPath, tickets: ticketsPath }
|
|
43
|
+
};
|
|
44
|
+
await registerDatabaseTable(nuxt, {
|
|
45
|
+
resolver,
|
|
46
|
+
schema: (dialect) => `./runtime/server/db/schema.${dialect}`
|
|
47
|
+
});
|
|
48
|
+
addServerTemplate({
|
|
49
|
+
filename: "#events/membership",
|
|
50
|
+
getContents: () => withMembership ? `export { hasActiveSubscription } from '@frigear-nu/subscriptions/utils'` : `export const hasActiveSubscription = async () => false`
|
|
51
|
+
});
|
|
52
|
+
addTypeTemplate({
|
|
53
|
+
filename: "types/events-membership.d.ts",
|
|
54
|
+
getContents: () => `declare module '#events/membership' {
|
|
55
|
+
export function hasActiveSubscription(userId: number, options?: { plans?: string[], statuses?: string[] }): Promise<boolean>
|
|
56
|
+
}
|
|
57
|
+
`
|
|
58
|
+
});
|
|
59
|
+
const route = (method, path, file) => addServerHandler({ route: `/api/events${path}`, method, handler: resolver.resolve(`./runtime/server/routes/${file}`) });
|
|
60
|
+
route("get", "", "public/events.get");
|
|
61
|
+
route("get", "/tickets/me", "public/tickets-me.get");
|
|
62
|
+
route("get", "/tickets/:id", "public/ticket.get");
|
|
63
|
+
route("post", "/tickets/:id/pay", "public/ticket-pay.post");
|
|
64
|
+
route("post", "/check-in", "public/check-in.post");
|
|
65
|
+
route("get", "/:slug", "public/event.get");
|
|
66
|
+
route("post", "/:slug/purchase", "public/purchase.post");
|
|
67
|
+
route("get", "/admin/events", "admin/events.get");
|
|
68
|
+
route("post", "/admin/events", "admin/events.post");
|
|
69
|
+
route("patch", "/admin/events/:id", "admin/event.patch");
|
|
70
|
+
route("delete", "/admin/events/:id", "admin/event.delete");
|
|
71
|
+
route("get", "/admin/events/:id/tickets", "admin/attendees.get");
|
|
72
|
+
route("post", "/admin/events/:id/ticket-types", "admin/ticket-types.post");
|
|
73
|
+
route("patch", "/admin/ticket-types/:id", "admin/ticket-type.patch");
|
|
74
|
+
route("delete", "/admin/ticket-types/:id", "admin/ticket-type.delete");
|
|
75
|
+
route("post", "/admin/ticket-types/:id/addon-groups", "admin/addon-groups.post");
|
|
76
|
+
route("patch", "/admin/addon-groups/:id", "admin/addon-group.patch");
|
|
77
|
+
route("delete", "/admin/addon-groups/:id", "admin/addon-group.delete");
|
|
78
|
+
route("post", "/admin/addon-groups/:id/addons", "admin/addons.post");
|
|
79
|
+
route("patch", "/admin/addons/:id", "admin/addon.patch");
|
|
80
|
+
route("delete", "/admin/addons/:id", "admin/addon.delete");
|
|
81
|
+
route("post", "/admin/tickets/:id/cancel", "admin/ticket-cancel.post");
|
|
82
|
+
route("post", "/admin/tickets/:id/refund", "admin/ticket-refund.post");
|
|
83
|
+
addServerImportsDir(resolver.resolve("./runtime/server/utils"));
|
|
84
|
+
addServerPlugin(resolver.resolve("./runtime/server/plugins/payment-hooks"));
|
|
85
|
+
addImports([
|
|
86
|
+
{ name: "useEvents", from: resolver.resolve("./runtime/app/composables/useEvents") },
|
|
87
|
+
{ name: "useEvent", from: resolver.resolve("./runtime/app/composables/useEvents") },
|
|
88
|
+
{ name: "useMyTickets", from: resolver.resolve("./runtime/app/composables/useEvents") },
|
|
89
|
+
{ name: "useEventsLocale", from: resolver.resolve("./runtime/app/composables/useEvents") }
|
|
90
|
+
]);
|
|
91
|
+
addComponentsDir({ path: resolver.resolve("./runtime/app/components") });
|
|
92
|
+
if (eventsPath || eventPath || ticketsPath) {
|
|
93
|
+
extendPages((pages) => {
|
|
94
|
+
const existingPaths = new Set(pages.map((page) => page.path));
|
|
95
|
+
if (eventsPath && !existingPaths.has(eventsPath)) {
|
|
96
|
+
pages.push(defineModulePage(MODULE_NAME, { name: "events-list", path: eventsPath, file: resolver.resolve("./runtime/app/pages/events.vue") }));
|
|
97
|
+
}
|
|
98
|
+
if (eventPath && !existingPaths.has(eventPath)) {
|
|
99
|
+
pages.push(defineModulePage(MODULE_NAME, { name: "events-detail", path: eventPath, file: resolver.resolve("./runtime/app/pages/event.vue") }));
|
|
100
|
+
}
|
|
101
|
+
if (ticketsPath && !existingPaths.has(ticketsPath)) {
|
|
102
|
+
pages.push(defineModulePage(MODULE_NAME, { name: "events-tickets", path: ticketsPath, file: resolver.resolve("./runtime/app/pages/tickets.vue") }));
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (withMenus) {
|
|
107
|
+
const { registerMenuSource } = await import('@frigear-nu/menu/utils');
|
|
108
|
+
registerMenuSource(nuxt, { file: resolver.resolve("./runtime/menus"), menu: "dashboard.account" });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export { module$1 as default };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EventSummaryDto } from '../../../types.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
event: EventSummaryDto;
|
|
4
|
+
to: string;
|
|
5
|
+
};
|
|
6
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
7
|
+
declare const _default: typeof __VLS_export;
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { translate } from "../../utils";
|
|
3
|
+
const props = defineProps({
|
|
4
|
+
event: { type: Object, required: true },
|
|
5
|
+
to: { type: String, required: true }
|
|
6
|
+
});
|
|
7
|
+
const t = useModuleT("events");
|
|
8
|
+
const locale = useEventsLocale();
|
|
9
|
+
const date = computed(() => new Intl.DateTimeFormat(locale.value, { dateStyle: "full", timeStyle: "short" }).format(new Date(props.event.startsAt)));
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<UCard :ui="{ body: 'flex flex-col gap-2' }">
|
|
14
|
+
<img
|
|
15
|
+
v-if="event.image"
|
|
16
|
+
:src="event.image"
|
|
17
|
+
:alt="translate(event.name, locale)"
|
|
18
|
+
class="aspect-[3/1] w-full rounded-md object-cover"
|
|
19
|
+
>
|
|
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">
|
|
35
|
+
<UBadge
|
|
36
|
+
v-if="event.requiresMembership"
|
|
37
|
+
color="warning"
|
|
38
|
+
variant="subtle"
|
|
39
|
+
icon="i-lucide-user-star"
|
|
40
|
+
>
|
|
41
|
+
{{ t("members", "Members") }}
|
|
42
|
+
</UBadge>
|
|
43
|
+
<UButton
|
|
44
|
+
:to="to"
|
|
45
|
+
class="ml-auto"
|
|
46
|
+
trailing-icon="i-lucide-arrow-right"
|
|
47
|
+
>
|
|
48
|
+
{{ t("viewEvent", "View event") }}
|
|
49
|
+
</UButton>
|
|
50
|
+
</div>
|
|
51
|
+
</UCard>
|
|
52
|
+
</template>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EventSummaryDto } from '../../../types.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
event: EventSummaryDto;
|
|
4
|
+
to: string;
|
|
5
|
+
};
|
|
6
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
7
|
+
declare const _default: typeof __VLS_export;
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TicketDto } from '../../../types.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
ticket: TicketDto;
|
|
4
|
+
to?: string;
|
|
5
|
+
};
|
|
6
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
7
|
+
pay: (ticketId: string) => any;
|
|
8
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
9
|
+
onPay?: ((ticketId: string) => any) | undefined;
|
|
10
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
|
+
declare const _default: typeof __VLS_export;
|
|
12
|
+
export default _default;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { renderSVG } from "uqr";
|
|
3
|
+
import { formatPrice, translate } from "../../utils";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
ticket: { type: Object, required: true },
|
|
6
|
+
to: { type: String, required: false }
|
|
7
|
+
});
|
|
8
|
+
const emit = defineEmits(["pay"]);
|
|
9
|
+
const t = useModuleT("events");
|
|
10
|
+
const locale = useEventsLocale();
|
|
11
|
+
const colors = { paid: "success", pending: "warning", cancelled: "neutral", expired: "neutral", refunded: "error" };
|
|
12
|
+
const qr = computed(() => props.ticket.code ? `data:image/svg+xml;utf8,${encodeURIComponent(renderSVG(props.ticket.code, { border: 1 }))}` : null);
|
|
13
|
+
const showQr = ref(false);
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<UCard :ui="{ body: 'flex flex-col gap-3' }">
|
|
18
|
+
<div class="flex items-start gap-2">
|
|
19
|
+
<div>
|
|
20
|
+
<h3 class="font-semibold">
|
|
21
|
+
{{ translate(ticket.eventName, locale) }}
|
|
22
|
+
</h3>
|
|
23
|
+
<p class="text-sm text-muted">
|
|
24
|
+
{{ translate(ticket.ticketTypeName, locale) }}<template
|
|
25
|
+
v-for="addon in ticket.addons"
|
|
26
|
+
:key="addon.id"
|
|
27
|
+
>
|
|
28
|
+
· {{ translate(addon.label, locale) }}
|
|
29
|
+
</template>
|
|
30
|
+
</p>
|
|
31
|
+
</div>
|
|
32
|
+
<UBadge
|
|
33
|
+
class="ml-auto capitalize"
|
|
34
|
+
:color="colors[ticket.status] ?? 'neutral'"
|
|
35
|
+
variant="subtle"
|
|
36
|
+
>
|
|
37
|
+
{{ t(`tickets.status.${ticket.status}`, ticket.status) }}
|
|
38
|
+
</UBadge>
|
|
39
|
+
</div>
|
|
40
|
+
<p class="text-sm">
|
|
41
|
+
{{ ticket.amount === 0 ? t("detail.tickets.free", "Free") : formatPrice(ticket.amount, ticket.currency, locale) }}
|
|
42
|
+
<UBadge
|
|
43
|
+
v-if="ticket.checkedInAt"
|
|
44
|
+
color="info"
|
|
45
|
+
variant="subtle"
|
|
46
|
+
class="ml-2"
|
|
47
|
+
>
|
|
48
|
+
{{ t("checkedIn", "Checked in") }}
|
|
49
|
+
</UBadge>
|
|
50
|
+
</p>
|
|
51
|
+
<div class="flex gap-2">
|
|
52
|
+
<UButton
|
|
53
|
+
v-if="ticket.status === 'pending'"
|
|
54
|
+
@click="emit('pay', ticket.id)"
|
|
55
|
+
>
|
|
56
|
+
{{ t("tickets.payNow", "Pay now") }}
|
|
57
|
+
</UButton>
|
|
58
|
+
<UButton
|
|
59
|
+
v-if="qr"
|
|
60
|
+
variant="subtle"
|
|
61
|
+
icon="i-lucide-qr-code"
|
|
62
|
+
@click="showQr = !showQr"
|
|
63
|
+
>
|
|
64
|
+
{{ showQr ? t("hideQr", "Hide QR code") : t("tickets.viewQrCode", "Show QR code") }}
|
|
65
|
+
</UButton>
|
|
66
|
+
<UButton
|
|
67
|
+
v-if="to"
|
|
68
|
+
:to="to"
|
|
69
|
+
variant="ghost"
|
|
70
|
+
class="ml-auto"
|
|
71
|
+
>
|
|
72
|
+
{{ t("viewEvent", "View event") }}
|
|
73
|
+
</UButton>
|
|
74
|
+
</div>
|
|
75
|
+
<img
|
|
76
|
+
v-if="showQr && qr"
|
|
77
|
+
:src="qr"
|
|
78
|
+
alt="Ticket QR code"
|
|
79
|
+
class="mx-auto w-56 rounded-md bg-white p-2"
|
|
80
|
+
>
|
|
81
|
+
</UCard>
|
|
82
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TicketDto } from '../../../types.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
ticket: TicketDto;
|
|
4
|
+
to?: string;
|
|
5
|
+
};
|
|
6
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
7
|
+
pay: (ticketId: string) => any;
|
|
8
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
9
|
+
onPay?: ((ticketId: string) => any) | undefined;
|
|
10
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
|
+
declare const _default: typeof __VLS_export;
|
|
12
|
+
export default _default;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TicketTypeDto } from '../../../types.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
ticketTypes: TicketTypeDto[];
|
|
4
|
+
loggedIn: boolean;
|
|
5
|
+
loading?: boolean;
|
|
6
|
+
};
|
|
7
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
8
|
+
purchase: (ticketTypeId: string, addonIds: string[]) => any;
|
|
9
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
10
|
+
onPurchase?: ((ticketTypeId: string, addonIds: string[]) => any) | undefined;
|
|
11
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
12
|
+
declare const _default: typeof __VLS_export;
|
|
13
|
+
export default _default;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { formatPrice, translate } from "../../utils";
|
|
3
|
+
const props = defineProps({
|
|
4
|
+
ticketTypes: { type: Array, required: true },
|
|
5
|
+
loggedIn: { type: Boolean, required: true },
|
|
6
|
+
loading: { type: Boolean, required: false }
|
|
7
|
+
});
|
|
8
|
+
const emit = defineEmits(["purchase"]);
|
|
9
|
+
const locale = useEventsLocale();
|
|
10
|
+
const selectedId = ref();
|
|
11
|
+
const chosen = ref({});
|
|
12
|
+
const selected = computed(() => props.ticketTypes.find((type) => type.id === selectedId.value));
|
|
13
|
+
watch(() => props.ticketTypes, (types) => {
|
|
14
|
+
if (!selectedId.value || !types.some((type) => type.id === selectedId.value)) {
|
|
15
|
+
selectedId.value = (types.find((type) => type.isDefault && isAvailable(type)) ?? types.find(isAvailable))?.id;
|
|
16
|
+
}
|
|
17
|
+
}, { immediate: true });
|
|
18
|
+
watch(selectedId, () => {
|
|
19
|
+
chosen.value = {};
|
|
20
|
+
});
|
|
21
|
+
function isAvailable(type) {
|
|
22
|
+
return type.saleState === "open" && !type.blocked;
|
|
23
|
+
}
|
|
24
|
+
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
|
+
return null;
|
|
31
|
+
};
|
|
32
|
+
const addonIds = computed(() => Object.values(chosen.value).flat());
|
|
33
|
+
const missingRequired = computed(() => selected.value?.addonGroups.some((group) => group.required && !chosen.value[group.id]?.length) ?? false);
|
|
34
|
+
const total = computed(() => (selected.value?.price ?? 0) + (selected.value?.addonGroups ?? []).flatMap((group) => group.addons).filter((addon) => addonIds.value.includes(addon.id)).reduce((sum, addon) => sum + addon.price, 0));
|
|
35
|
+
function toggle(groupId, addonId, multiple) {
|
|
36
|
+
const current = chosen.value[groupId] ?? [];
|
|
37
|
+
chosen.value = {
|
|
38
|
+
...chosen.value,
|
|
39
|
+
[groupId]: multiple ? current.includes(addonId) ? current.filter((id) => id !== addonId) : [...current, addonId] : [addonId]
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<div class="flex flex-col gap-4">
|
|
46
|
+
<button
|
|
47
|
+
v-for="type in ticketTypes"
|
|
48
|
+
:key="type.id"
|
|
49
|
+
type="button"
|
|
50
|
+
class="rounded-lg border p-4 text-left transition disabled:cursor-not-allowed disabled:opacity-60"
|
|
51
|
+
:class="selectedId === type.id ? 'border-primary ring-1 ring-primary' : 'border-default'"
|
|
52
|
+
:disabled="!isAvailable(type)"
|
|
53
|
+
@click="selectedId = type.id"
|
|
54
|
+
>
|
|
55
|
+
<div class="flex items-center gap-2">
|
|
56
|
+
<span class="font-medium">{{ translate(type.name, locale) }}</span>
|
|
57
|
+
<UBadge
|
|
58
|
+
v-if="reason(type)"
|
|
59
|
+
color="neutral"
|
|
60
|
+
variant="subtle"
|
|
61
|
+
>
|
|
62
|
+
{{ reason(type) }}
|
|
63
|
+
</UBadge>
|
|
64
|
+
<UBadge
|
|
65
|
+
v-else-if="type.remaining !== null && type.remaining <= 10"
|
|
66
|
+
color="warning"
|
|
67
|
+
variant="subtle"
|
|
68
|
+
>
|
|
69
|
+
{{ type.remaining }} left
|
|
70
|
+
</UBadge>
|
|
71
|
+
<span
|
|
72
|
+
v-if="!type.hidePrice"
|
|
73
|
+
class="ml-auto"
|
|
74
|
+
>{{ type.price === 0 ? "Free" : formatPrice(type.price, type.currency, locale) }}</span>
|
|
75
|
+
</div>
|
|
76
|
+
<p
|
|
77
|
+
v-if="type.description"
|
|
78
|
+
class="mt-1 text-sm text-muted"
|
|
79
|
+
>
|
|
80
|
+
{{ translate(type.description, locale) }}
|
|
81
|
+
</p>
|
|
82
|
+
</button>
|
|
83
|
+
|
|
84
|
+
<fieldset
|
|
85
|
+
v-for="group in selected?.addonGroups ?? []"
|
|
86
|
+
:key="group.id"
|
|
87
|
+
class="flex flex-col gap-2"
|
|
88
|
+
>
|
|
89
|
+
<legend class="mb-1 font-medium">
|
|
90
|
+
{{ translate(group.title, locale) }}<span
|
|
91
|
+
v-if="group.required"
|
|
92
|
+
class="text-error"
|
|
93
|
+
> *</span>
|
|
94
|
+
</legend>
|
|
95
|
+
<button
|
|
96
|
+
v-for="addon in group.addons"
|
|
97
|
+
:key="addon.id"
|
|
98
|
+
type="button"
|
|
99
|
+
class="flex items-center gap-2 rounded-md border p-3 text-left disabled:opacity-60"
|
|
100
|
+
:class="chosen[group.id]?.includes(addon.id) ? 'border-primary ring-1 ring-primary' : 'border-default'"
|
|
101
|
+
:disabled="addon.soldOut"
|
|
102
|
+
@click="toggle(group.id, addon.id, group.multiple)"
|
|
103
|
+
>
|
|
104
|
+
<span>{{ translate(addon.label, locale) }}</span>
|
|
105
|
+
<UBadge
|
|
106
|
+
v-if="addon.soldOut"
|
|
107
|
+
color="neutral"
|
|
108
|
+
variant="subtle"
|
|
109
|
+
>
|
|
110
|
+
Sold out
|
|
111
|
+
</UBadge>
|
|
112
|
+
<span class="ml-auto text-sm">{{ addon.price === 0 ? "Included" : `+ ${formatPrice(addon.price, selected.currency, locale)}` }}</span>
|
|
113
|
+
</button>
|
|
114
|
+
</fieldset>
|
|
115
|
+
|
|
116
|
+
<UButton
|
|
117
|
+
v-if="loggedIn"
|
|
118
|
+
size="lg"
|
|
119
|
+
block
|
|
120
|
+
:loading="loading"
|
|
121
|
+
:disabled="!selected || !isAvailable(selected) || missingRequired"
|
|
122
|
+
@click="emit('purchase', selected.id, addonIds)"
|
|
123
|
+
>
|
|
124
|
+
{{ total === 0 ? "Get free ticket" : `Buy ticket \xB7 ${formatPrice(total, selected?.currency ?? "DKK", locale)}` }}
|
|
125
|
+
</UButton>
|
|
126
|
+
<UAlert
|
|
127
|
+
v-else
|
|
128
|
+
color="neutral"
|
|
129
|
+
variant="subtle"
|
|
130
|
+
title="Sign in to get a ticket."
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
</template>
|