@frigear-nu/events 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +1 -1
  2. package/dist/module.d.mts +45 -7
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +53 -42
  5. package/dist/runtime/app/components/EventAdminAddonDialog.d.vue.ts +11 -0
  6. package/dist/runtime/app/components/EventAdminAddonDialog.vue +113 -0
  7. package/dist/runtime/app/components/EventAdminAddonDialog.vue.d.ts +11 -0
  8. package/dist/runtime/app/components/EventAdminAddonGroupDialog.d.vue.ts +11 -0
  9. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue +79 -0
  10. package/dist/runtime/app/components/EventAdminAddonGroupDialog.vue.d.ts +11 -0
  11. package/dist/runtime/app/components/EventAdminAddonGroups.d.vue.ts +19 -0
  12. package/dist/runtime/app/components/EventAdminAddonGroups.vue +123 -0
  13. package/dist/runtime/app/components/EventAdminAddonGroups.vue.d.ts +19 -0
  14. package/dist/runtime/app/components/EventAdminTicketTypeDialog.d.vue.ts +20 -0
  15. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue +175 -0
  16. package/dist/runtime/app/components/EventAdminTicketTypeDialog.vue.d.ts +20 -0
  17. package/dist/runtime/app/components/EventCard.vue +39 -27
  18. package/dist/runtime/app/components/EventTicketCard.vue +5 -4
  19. package/dist/runtime/app/components/EventTicketPicker.vue +40 -21
  20. package/dist/runtime/app/composables/index.d.ts +1 -1
  21. package/dist/runtime/app/composables/index.js +1 -1
  22. package/dist/runtime/app/composables/useEvents.d.ts +10 -4
  23. package/dist/runtime/app/composables/useEvents.js +21 -10
  24. package/dist/runtime/app/pages/admin-event.d.vue.ts +3 -0
  25. package/dist/runtime/app/pages/admin-event.vue +151 -0
  26. package/dist/runtime/app/pages/admin-event.vue.d.ts +3 -0
  27. package/dist/runtime/app/pages/admin.d.vue.ts +3 -0
  28. package/dist/runtime/app/pages/admin.vue +154 -0
  29. package/dist/runtime/app/pages/admin.vue.d.ts +3 -0
  30. package/dist/runtime/app/pages/event.d.vue.ts +14 -1
  31. package/dist/runtime/app/pages/event.vue +160 -45
  32. package/dist/runtime/app/pages/event.vue.d.ts +14 -1
  33. package/dist/runtime/app/pages/events.vue +12 -10
  34. package/dist/runtime/app/pages/tickets.vue +26 -19
  35. package/dist/runtime/app/tabs/Attendees.d.vue.ts +7 -0
  36. package/dist/runtime/app/tabs/Attendees.vue +88 -0
  37. package/dist/runtime/app/tabs/Attendees.vue.d.ts +7 -0
  38. package/dist/runtime/app/tabs/Details.d.vue.ts +11 -0
  39. package/dist/runtime/app/tabs/Details.vue +74 -0
  40. package/dist/runtime/app/tabs/Details.vue.d.ts +11 -0
  41. package/dist/runtime/app/tabs/Tickets.d.vue.ts +11 -0
  42. package/dist/runtime/app/tabs/Tickets.vue +164 -0
  43. package/dist/runtime/app/tabs/Tickets.vue.d.ts +11 -0
  44. package/dist/runtime/app/utils/admin-event.d.ts +48 -0
  45. package/dist/runtime/app/utils/admin-event.js +22 -0
  46. package/dist/runtime/i18n/lang/da.json +34 -1
  47. package/dist/runtime/i18n/lang/en.json +33 -0
  48. package/dist/runtime/menus.js +17 -1
  49. package/dist/runtime/server/db/schema.sqlite.d.ts +62 -5
  50. package/dist/runtime/server/db/schema.sqlite.js +27 -2
  51. package/dist/runtime/server/mcp/schemas.d.ts +119 -0
  52. package/dist/runtime/server/mcp/schemas.js +58 -0
  53. package/dist/runtime/server/mcp/tools/admin/create-event.d.ts +2 -0
  54. package/dist/runtime/server/mcp/tools/admin/create-event.js +11 -0
  55. package/dist/runtime/server/mcp/tools/admin/get-event-definition.d.ts +2 -0
  56. package/dist/runtime/server/mcp/tools/admin/get-event-definition.js +10 -0
  57. package/dist/runtime/server/mcp/tools/admin/list-all-events.d.ts +2 -0
  58. package/dist/runtime/server/mcp/tools/admin/list-all-events.js +9 -0
  59. package/dist/runtime/server/mcp/tools/admin/list-attendees.d.ts +2 -0
  60. package/dist/runtime/server/mcp/tools/admin/list-attendees.js +25 -0
  61. package/dist/runtime/server/mcp/tools/admin/update-event.d.ts +2 -0
  62. package/dist/runtime/server/mcp/tools/admin/update-event.js +10 -0
  63. package/dist/runtime/server/mcp/tools/admin/update-ticket-type.d.ts +2 -0
  64. package/dist/runtime/server/mcp/tools/admin/update-ticket-type.js +10 -0
  65. package/dist/runtime/server/mcp/tools/get-event.d.ts +2 -0
  66. package/dist/runtime/server/mcp/tools/get-event.js +12 -0
  67. package/dist/runtime/server/mcp/tools/list-events.d.ts +2 -0
  68. package/dist/runtime/server/mcp/tools/list-events.js +6 -0
  69. package/dist/runtime/server/mcp/tools/my-tickets.d.ts +2 -0
  70. package/dist/runtime/server/mcp/tools/my-tickets.js +6 -0
  71. package/dist/runtime/server/plugins/staff-scopes.d.ts +2 -0
  72. package/dist/runtime/server/plugins/staff-scopes.js +1 -0
  73. package/dist/runtime/server/plugins/subscriptions-membership.d.ts +2 -0
  74. package/dist/runtime/server/plugins/subscriptions-membership.js +11 -0
  75. package/dist/runtime/server/routes/admin/event-clone.post.d.ts +2 -0
  76. package/dist/runtime/server/routes/admin/event-clone.post.js +31 -0
  77. package/dist/runtime/server/routes/admin/event.get.d.ts +2 -0
  78. package/dist/runtime/server/routes/admin/event.get.js +5 -0
  79. package/dist/runtime/server/routes/admin/event.patch.js +3 -8
  80. package/dist/runtime/server/routes/admin/events.post.js +3 -13
  81. package/dist/runtime/server/routes/admin/membership-options.get.d.ts +2 -0
  82. package/dist/runtime/server/routes/admin/membership-options.get.js +4 -0
  83. package/dist/runtime/server/routes/admin/ticket-type.patch.js +3 -8
  84. package/dist/runtime/server/routes/admin/ticket-types.post.js +2 -1
  85. package/dist/runtime/server/routes/public/event.get.js +2 -2
  86. package/dist/runtime/server/routes/public/purchase.post.js +3 -2
  87. package/dist/runtime/server/routes/public/ticket-pay.post.js +1 -1
  88. package/dist/runtime/server/routes/public/ticket.get.js +1 -1
  89. package/dist/runtime/server/routes/public/tickets-me.get.js +1 -1
  90. package/dist/runtime/server/utils/admin-schemas.d.ts +102 -20
  91. package/dist/runtime/server/utils/admin-schemas.js +8 -2
  92. package/dist/runtime/server/utils/admin.d.ts +82 -1
  93. package/dist/runtime/server/utils/admin.js +51 -1
  94. package/dist/runtime/server/utils/auth.d.ts +7 -6
  95. package/dist/runtime/server/utils/auth.js +4 -4
  96. package/dist/runtime/server/utils/catalog.d.ts +14 -0
  97. package/dist/runtime/server/utils/catalog.js +20 -4
  98. package/dist/runtime/server/utils/membership.d.ts +6 -2
  99. package/dist/runtime/server/utils/membership.js +15 -2
  100. package/dist/runtime/server/utils/tickets.js +15 -12
  101. package/dist/runtime/utils/index.d.ts +17 -7
  102. package/dist/runtime/utils/index.js +16 -19
  103. package/dist/types.d.mts +3 -1
  104. package/package.json +21 -7
@@ -1,3 +1,7 @@
1
- import type { MembershipRequirement } from '../../types.js';
2
- /** True when `requirement` is null, or the user holds a matching active subscription. */
1
+ import type { MembershipOptionsContext, MembershipRequirement } from '../../types.js';
2
+ /** True when `requirement` is null, or a membership provider confirms the user holds a matching membership. */
3
3
  export declare function meetsMembership(userId: number, requirement: MembershipRequirement | undefined): Promise<boolean>;
4
+ /** True when `exclusion` is null, or the user holds none of the excluded memberships (`'any'` = no membership at all). */
5
+ export declare function passesExclusion(userId: number, exclusion: MembershipRequirement | undefined): Promise<boolean>;
6
+ /** What the installed membership provider(s) offer; `available` is false when there is none. */
7
+ export declare function getMembershipOptions(): Promise<MembershipOptionsContext>;
@@ -1,5 +1,18 @@
1
- import { hasActiveSubscription } from "#events/membership";
1
+ async function holds(userId, requirement) {
2
+ const ctx = { userId, requirement, satisfied: false };
3
+ await useNitroApp().hooks.callHook("events:membership:check", ctx);
4
+ return ctx.satisfied;
5
+ }
2
6
  export async function meetsMembership(userId, requirement) {
3
7
  if (!requirement) return true;
4
- return hasActiveSubscription(userId, requirement === "any" ? {} : { plans: requirement });
8
+ return holds(userId, requirement);
9
+ }
10
+ export async function passesExclusion(userId, exclusion) {
11
+ if (!exclusion) return true;
12
+ return !await holds(userId, exclusion);
13
+ }
14
+ export async function getMembershipOptions() {
15
+ const ctx = { available: false, options: [] };
16
+ await useNitroApp().hooks.callHook("events:membership:options", ctx);
17
+ return ctx;
5
18
  }
@@ -1,8 +1,9 @@
1
1
  import { db, schema } from "@nuxthub/db";
2
2
  import { and, eq, sql } from "drizzle-orm";
3
- import { newId, newTicketCode, normalizeMembership, resolveSameOriginUrl, translate } from "../../utils/index.js";
3
+ import { resolveSameOriginUrl } from "@frigear-nu/stripe/utils";
4
+ import { newId, newTicketCode, normalizeMembership, translate } from "../../utils/index.js";
4
5
  import { countAddonSeats, countSeats, getPublishedEventRow, holdsSeat, loadCatalog, toTicketDtos } from "./catalog.js";
5
- import { meetsMembership } from "./membership.js";
6
+ import { meetsMembership, passesExclusion } from "./membership.js";
6
7
  const fail = (statusCode, statusMessage) => createError({ statusCode, statusMessage });
7
8
  function returnUrls(event, slug, ticketId, body) {
8
9
  const origin = getRequestURL(event).origin;
@@ -30,13 +31,14 @@ export async function purchaseTicket(event, userId, slug, body) {
30
31
  const { types, groups, addons } = await loadCatalog(eventRow.id);
31
32
  const type = types.find((item) => item.id === body.ticketTypeId);
32
33
  if (!type) throw fail(404, "Ticket type not found.");
33
- const now = Date.now();
34
- if (type.saleStartsAt && type.saleStartsAt.getTime() > now) throw fail(409, "Ticket sales have not started yet.");
35
- if (type.saleEndsAt && type.saleEndsAt.getTime() < now) throw fail(409, "Ticket sales have ended.");
34
+ const window = saleWindow(type);
35
+ if (window.state === "not_started") throw fail(409, "Ticket sales have not started yet.");
36
+ if (window.state === "ended") throw fail(409, "Ticket sales have ended.");
36
37
  const requirements = [normalizeMembership(eventRow.requiresMembership), normalizeMembership(type.requiresMembership)];
37
38
  for (const requirement of requirements) {
38
39
  if (!await meetsMembership(userId, requirement)) throw fail(403, "An active membership is required for this ticket.");
39
40
  }
41
+ if (!await passesExclusion(userId, normalizeMembership(type.excludesMembership))) throw fail(403, "This ticket is not available for your membership.");
40
42
  const typeGroups = groups.filter((group) => group.ticketTypeId === type.id);
41
43
  const typeAddons = addons.filter((addon) => typeGroups.some((group) => group.id === addon.groupId));
42
44
  const addonIds = [...new Set(body.addonIds ?? [])];
@@ -47,11 +49,11 @@ export async function purchaseTicket(event, userId, slug, body) {
47
49
  });
48
50
  for (const group of typeGroups) {
49
51
  const inGroup = chosen.filter((addon) => addon.groupId === group.id);
50
- if (group.required && inGroup.length === 0) throw fail(400, `Choose an option for "${translate(group.title)}".`);
51
- if (!group.multiple && inGroup.length > 1) throw fail(400, `Only one option can be chosen for "${translate(group.title)}".`);
52
+ if (group.required && inGroup.length === 0) throw fail(400, `Choose an option for "${translate(group.title, body.locale)}".`);
53
+ if (!group.multiple && inGroup.length > 1) throw fail(400, `Only one option can be chosen for "${translate(group.title, body.locale)}".`);
52
54
  }
53
55
  for (const addon of chosen) {
54
- if (addon.capacity !== null && await countAddonSeats(addon.id) >= addon.capacity) throw fail(409, `"${translate(addon.label)}" is sold out.`);
56
+ if (addon.capacity !== null && await countAddonSeats(addon.id) >= addon.capacity) throw fail(409, `"${translate(addon.label, body.locale)}" is sold out.`);
55
57
  }
56
58
  if (type.capacity !== null && await countSeats(type.id) >= type.capacity) throw fail(409, "This ticket type is sold out.");
57
59
  await useNitroApp().hooks.callHook("events:purchase:before", { event, userId, eventId: eventRow.id, ticketTypeId: type.id, addonIds });
@@ -64,6 +66,7 @@ export async function purchaseTicket(event, userId, slug, body) {
64
66
  eventId: eventRow.id,
65
67
  ticketTypeId: type.id,
66
68
  userId,
69
+ locale: body.locale ?? null,
67
70
  status: amount === 0 ? "paid" : "pending",
68
71
  addons: chosen.map((addon) => ({ id: addon.id, label: addon.label, price: addon.price })),
69
72
  amount,
@@ -81,7 +84,7 @@ export async function purchaseTicket(event, userId, slug, body) {
81
84
  const winners = await db.select({ id: schema.eventTickets.id }).from(schema.eventTickets).where(and(sql`${schema.eventTickets.addons} like ${`%"id":"${addon.id}"%`}`, holdsSeat())).orderBy(sql`rowid`).limit(addon.capacity);
82
85
  if (!winners.some((winner) => winner.id === id)) {
83
86
  await release(id);
84
- throw fail(409, `"${translate(addon.label)}" is sold out.`);
87
+ throw fail(409, `"${translate(addon.label, body.locale)}" is sold out.`);
85
88
  }
86
89
  }
87
90
  const [dto] = await toTicketDtos([inserted]);
@@ -93,10 +96,10 @@ export async function purchaseTicket(event, userId, slug, body) {
93
96
  const urls = returnUrls(event, eventRow.slug, id, body);
94
97
  const { payment, url } = await createPayment({
95
98
  userId,
96
- description: `${translate(eventRow.name)} - ${translate(type.name)}`,
99
+ description: `${translate(eventRow.name, body.locale)} - ${translate(type.name, body.locale)}`,
97
100
  items: [
98
- ...type.price > 0 ? [{ name: `${translate(eventRow.name)} - ${translate(type.name)}`, amount: type.price, currency: type.currency }] : [],
99
- ...chosen.filter((addon) => addon.price > 0).map((addon) => ({ name: translate(addon.label), amount: addon.price, currency: type.currency }))
101
+ ...type.price > 0 ? [{ name: `${translate(eventRow.name, body.locale)} - ${translate(type.name, body.locale)}`, amount: type.price, currency: type.currency }] : [],
102
+ ...chosen.filter((addon) => addon.price > 0).map((addon) => ({ name: translate(addon.label, body.locale), amount: addon.price, currency: type.currency }))
100
103
  ],
101
104
  reference: { type: "event_ticket", id },
102
105
  expiresInMinutes: useRuntimeConfig().events.reservationMinutes,
@@ -1,14 +1,24 @@
1
- import type { MembershipRequirement, Translated } from '../types.js';
1
+ import { translate } from '@frigear-nu/ui/translated';
2
+ import type { MembershipRequirement } from '../types.js';
3
+ export { translate };
2
4
  export type { AddonDto, AddonGroupDto, AuthorizeContext, CheckInResponse, EventDetailDto, EventStatus, EventSummaryDto, MembershipRequirement, ModuleOptions, PurchaseBeforeContext, PurchaseBody, PurchaseResponse, TicketDto, TicketHookContext, TicketStatus, TicketTypeDto, Translated, } from '../types.js';
3
- /** Picks the text for a locale, falling back to `en`, then to the first available entry. */
4
- export declare function translate(value: Translated | null | undefined, locale?: string): string;
5
- /** Formats minor units, e.g. `formatPrice(15000, 'DKK', 'da')` -> `150,00 kr.`. */
6
- export declare function formatPrice(amount: number, currency: string, locale?: string): string;
5
+ /** The currency tickets and add-ons are sold in unless they say otherwise (the column default). */
6
+ export declare const DEFAULT_CURRENCY = "DKK";
7
7
  /** Normalises a stored requirement: empty plan lists mean "any active subscription". */
8
8
  export declare function normalizeMembership(value: MembershipRequirement | undefined): MembershipRequirement;
9
- /** Resolves a relative path against `origin`, and only accepts absolute URLs on the same origin. */
10
- export declare function resolveSameOriginUrl(input: string, origin: string): string | null;
11
9
  /** Random, prefixed id (`tkt_...`), Web Crypto only so it works on Workers. */
12
10
  export declare function newId(prefix: string): string;
13
11
  /** A longer random token for ticket QR codes. */
14
12
  export declare function newTicketCode(): string;
13
+ /**
14
+ * Variables a ticket type's description can use in MDC (`{{ticket}}`, `{{price}}`, ...).
15
+ * `ticket` is the ticket's name; the rest describe the same ticket.
16
+ */
17
+ export declare function ticketMdcData(type: {
18
+ name: unknown;
19
+ price: number;
20
+ currency: string;
21
+ hidePrice?: boolean;
22
+ remaining?: number | null;
23
+ key?: string;
24
+ }, name: string, locale?: string): Record<string, unknown>;
@@ -1,30 +1,27 @@
1
- export function translate(value, locale = "en") {
2
- if (!value) return "";
3
- if (typeof value === "string") return value;
4
- return value[locale] ?? value.en ?? Object.values(value)[0] ?? "";
5
- }
6
- export function formatPrice(amount, currency, locale = "en") {
7
- return new Intl.NumberFormat(locale, { style: "currency", currency }).format(amount / 100);
8
- }
1
+ import { translate } from "@frigear-nu/ui/translated";
2
+ import { formatMoney as formatPrice } from "@frigear-nu/ui/money";
3
+ export { translate };
4
+ export const DEFAULT_CURRENCY = "DKK";
9
5
  export function normalizeMembership(value) {
10
6
  if (value === "any") return "any";
11
7
  if (Array.isArray(value)) return value.length ? value : "any";
12
8
  return null;
13
9
  }
14
- export function resolveSameOriginUrl(input, origin) {
15
- if (input.startsWith("/")) {
16
- if (input.startsWith("//") || input.startsWith("/\\")) return null;
17
- return `${origin}${input}`;
18
- }
19
- try {
20
- return new URL(input).origin === new URL(origin).origin ? input : null;
21
- } catch {
22
- return null;
23
- }
24
- }
25
10
  export function newId(prefix) {
26
11
  return `${prefix}_${crypto.randomUUID().replaceAll("-", "")}`;
27
12
  }
28
13
  export function newTicketCode() {
29
14
  return `${crypto.randomUUID().replaceAll("-", "")}${crypto.randomUUID().replaceAll("-", "")}`;
30
15
  }
16
+ export function ticketMdcData(type, name, locale = "en") {
17
+ return {
18
+ ticket: name,
19
+ name,
20
+ key: type.key,
21
+ price: type.price === 0 ? "Free" : formatPrice(type.price, type.currency, locale),
22
+ amount: type.price,
23
+ currency: type.currency,
24
+ hidePrice: !!type.hidePrice,
25
+ remaining: type.remaining ?? null
26
+ };
27
+ }
package/dist/types.d.mts CHANGED
@@ -1,3 +1,5 @@
1
+ export { type Translated } from '@frigear-nu/ui/translated'
2
+
1
3
  export { default } from './module.mjs'
2
4
 
3
- export { type AddonDto, type AddonGroupDto, type AuthorizeContext, type CheckInResponse, type EventDetailDto, type EventStatus, type EventSummaryDto, type EventsRuntimeConfig, type MembershipRequirement, type ModuleOptions, type PublicModuleOptions, type PurchaseBeforeContext, type PurchaseBody, type PurchaseResponse, type TicketDto, type TicketHookContext, type TicketStatus, type TicketTypeDto, type Translated } from './module.mjs'
5
+ export { type AddonDto, type AddonGroupDto, type AuthorizeContext, type CheckInResponse, type EventDetailDto, type EventStatus, type EventSummaryDto, type EventsRuntimeConfig, type MembershipCheckContext, type MembershipOption, type MembershipOptionsContext, type MembershipRequirement, type ModuleOptions, type PublicModuleOptions, type PurchaseBeforeContext, type PurchaseBody, type PurchaseResponse, type TicketDto, type TicketHookContext, type TicketStatus, type TicketTypeDto } from './module.mjs'
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@frigear-nu/events",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Events with ticket types, addons, capacity, membership requirements, Stripe payments and QR check-in for the frigear ecosystem, built on @frigear-nu/payments.",
5
+ "homepage": "https://docs.frigear.dev/modules/events",
5
6
  "repository": {
6
7
  "type": "git",
7
8
  "url": "git+https://github.com/frigear-nu/frigear-nu.git",
@@ -29,12 +30,15 @@
29
30
  "playground"
30
31
  ],
31
32
  "dependencies": {
32
- "@frigear-nu/menu": "0.0.7",
33
- "@frigear-nu/module-kit": "0.1.5",
34
- "@frigear-nu/payments": "0.0.6",
35
- "@frigear-nu/stripe": "0.0.6",
36
- "@frigear-nu/subscriptions": "0.0.6",
37
- "@frigear-nu/users": "1.0.7",
33
+ "@frigear-nu/admin": "0.0.2",
34
+ "@frigear-nu/module-kit": "0.1.7",
35
+ "@frigear-nu/payments": "0.0.8",
36
+ "@frigear-nu/stripe": "0.0.8",
37
+ "@frigear-nu/subscriptions": "0.0.8",
38
+ "@frigear-nu/ui": "0.0.7",
39
+ "@frigear-nu/auth": "0.0.3",
40
+ "@frigear-nu/mcp": "0.0.3",
41
+ "@frigear-nu/users": "1.0.9",
38
42
  "@libsql/client": "^0.18.0",
39
43
  "@nuxt/kit": "^4.6.0",
40
44
  "@vueuse/core": "^15.0.0",
@@ -47,15 +51,25 @@
47
51
  "zod": "^4.6.5"
48
52
  },
49
53
  "peerDependencies": {
54
+ "@frigear-nu/menu": "0.0.9",
55
+ "@frigear-nu/widget": "0.0.2",
50
56
  "@nuxt/ui": "^4.10.0",
51
57
  "@nuxthub/core": "^0.10.8"
52
58
  },
53
59
  "peerDependenciesMeta": {
60
+ "@frigear-nu/menu": {
61
+ "optional": true
62
+ },
63
+ "@frigear-nu/widget": {
64
+ "optional": true
65
+ },
54
66
  "@nuxt/ui": {
55
67
  "optional": true
56
68
  }
57
69
  },
58
70
  "devDependencies": {
71
+ "@frigear-nu/menu": "0.0.9",
72
+ "@frigear-nu/widget": "0.0.2",
59
73
  "@nuxt/devtools": "^3.4.2",
60
74
  "@nuxt/eslint-config": "^1.17.0",
61
75
  "@nuxt/module-builder": "^1.0.3",