@frigear-nu/events 0.0.8 → 0.0.10
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/dist/module.json +1 -1
- package/dist/module.mjs +8 -1
- package/dist/runtime/app/widgets/UpcomingEvents.d.vue.ts +3 -0
- package/dist/runtime/app/widgets/UpcomingEvents.vue +39 -0
- package/dist/runtime/app/widgets/UpcomingEvents.vue.d.ts +3 -0
- package/dist/runtime/app/widgets/UpcomingEventsStat.d.vue.ts +3 -0
- package/dist/runtime/app/widgets/UpcomingEventsStat.vue +17 -0
- package/dist/runtime/app/widgets/UpcomingEventsStat.vue.d.ts +3 -0
- package/dist/runtime/i18n/lang/da.json +6 -0
- package/dist/runtime/i18n/lang/en.json +6 -0
- package/dist/runtime/server/db/schema.postgresql.d.ts +1110 -0
- package/dist/runtime/server/db/schema.postgresql.js +125 -0
- package/dist/runtime/server/mcp/money.d.ts +24 -0
- package/dist/runtime/server/mcp/money.js +42 -0
- package/dist/runtime/server/mcp/schemas.js +4 -3
- package/dist/runtime/server/mcp/tools/admin/create-event.js +3 -2
- package/dist/runtime/server/mcp/tools/admin/get-event-definition.js +3 -2
- package/dist/runtime/server/mcp/tools/admin/list-attendees.js +3 -2
- package/dist/runtime/server/mcp/tools/admin/update-ticket-type.js +9 -2
- package/dist/runtime/server/mcp/tools/get-event.js +3 -2
- package/dist/runtime/server/mcp/tools/my-tickets.js +3 -2
- package/dist/runtime/server/routes/admin/upcoming.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/upcoming.get.js +17 -0
- package/package.json +14 -14
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { pgTable, text, integer, boolean, jsonb, timestamp, index, uniqueIndex, customType } from "drizzle-orm/pg-core";
|
|
2
|
+
export const events = pgTable("events", {
|
|
3
|
+
/** `evn_` + random id. */
|
|
4
|
+
id: text().primaryKey(),
|
|
5
|
+
slug: text().notNull().unique(),
|
|
6
|
+
name: jsonb().$type().notNull(),
|
|
7
|
+
description: jsonb().$type(),
|
|
8
|
+
/** Markdown body (per locale). */
|
|
9
|
+
body: jsonb().$type(),
|
|
10
|
+
image: text(),
|
|
11
|
+
// A plain `text` column that also holds a `Translated`: strings are stored as they are (so rows written before
|
|
12
|
+
// locations could be translated keep working), per-locale maps as json. No schema change, hence no migration.
|
|
13
|
+
// Kept inline: module-kit lifts each table's columns into the host schema, so helpers declared above would be lost.
|
|
14
|
+
location: customType({
|
|
15
|
+
dataType: () => "text",
|
|
16
|
+
toDriver: (value) => typeof value === "string" ? value : JSON.stringify(value),
|
|
17
|
+
fromDriver: (value) => {
|
|
18
|
+
if (value.startsWith("{")) {
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(value);
|
|
21
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
})(),
|
|
28
|
+
locationUrl: text(),
|
|
29
|
+
startsAt: timestamp({ withTimezone: true }).notNull(),
|
|
30
|
+
endsAt: timestamp({ withTimezone: true }),
|
|
31
|
+
/** `draft | published | archived`. Only published events are public. */
|
|
32
|
+
status: text().notNull().default("draft"),
|
|
33
|
+
/** Same shape as `requiresMembership` on a ticket type; applies to every ticket of the event. */
|
|
34
|
+
requiresMembership: jsonb().$type(),
|
|
35
|
+
createdAt: timestamp({ withTimezone: true }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
36
|
+
updatedAt: timestamp({ withTimezone: true }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()).$onUpdateFn(() => /* @__PURE__ */ new Date())
|
|
37
|
+
}, (t) => [
|
|
38
|
+
index("event_status_starts_idx").on(t.status, t.startsAt)
|
|
39
|
+
]);
|
|
40
|
+
export const eventTicketTypes = pgTable("event_ticket_types", {
|
|
41
|
+
/** `ttp_` + random id. */
|
|
42
|
+
id: text().primaryKey(),
|
|
43
|
+
eventId: text().notNull().references(() => events.id, { onDelete: "cascade" }),
|
|
44
|
+
/** Stable key within the event (e.g. `default`, `premium`). */
|
|
45
|
+
key: text().notNull(),
|
|
46
|
+
name: jsonb().$type().notNull(),
|
|
47
|
+
description: jsonb().$type(),
|
|
48
|
+
/** Price in minor units; `0` is a free ticket. */
|
|
49
|
+
price: integer().notNull().default(0),
|
|
50
|
+
currency: text().notNull().default("DKK"),
|
|
51
|
+
hidePrice: boolean().notNull().default(false),
|
|
52
|
+
/** Maximum number of sold + reserved tickets; null = unlimited. */
|
|
53
|
+
capacity: integer(),
|
|
54
|
+
saleStartsAt: timestamp({ withTimezone: true }),
|
|
55
|
+
saleEndsAt: timestamp({ withTimezone: true }),
|
|
56
|
+
/** Optional recurring sale season (`yearly`) or fixed `range`, checked on top of `saleStartsAt`/`saleEndsAt`; null = always. */
|
|
57
|
+
saleSchedule: jsonb().$type(),
|
|
58
|
+
maxPerUser: integer().notNull().default(1),
|
|
59
|
+
/** `null` = open to everyone, `'any'` = any active subscription, or a list of plan keys. */
|
|
60
|
+
requiresMembership: jsonb().$type(),
|
|
61
|
+
/**
|
|
62
|
+
* The opposite: holders of these plans can NOT buy this ticket (`'any'` = any active subscription), so a
|
|
63
|
+
* member price and a non-member price can be separate ticket types. `null` = nobody is excluded.
|
|
64
|
+
*/
|
|
65
|
+
excludesMembership: jsonb().$type(),
|
|
66
|
+
isDefault: boolean().notNull().default(false),
|
|
67
|
+
sortOrder: integer().notNull().default(0),
|
|
68
|
+
createdAt: timestamp({ withTimezone: true }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
69
|
+
updatedAt: timestamp({ withTimezone: true }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()).$onUpdateFn(() => /* @__PURE__ */ new Date())
|
|
70
|
+
}, (t) => [
|
|
71
|
+
uniqueIndex("event_ticket_type_key_idx").on(t.eventId, t.key)
|
|
72
|
+
]);
|
|
73
|
+
export const eventAddonGroups = pgTable("event_addon_groups", {
|
|
74
|
+
/** `agr_` + random id. */
|
|
75
|
+
id: text().primaryKey(),
|
|
76
|
+
ticketTypeId: text().notNull().references(() => eventTicketTypes.id, { onDelete: "cascade" }),
|
|
77
|
+
title: jsonb().$type().notNull(),
|
|
78
|
+
/** When true, at least one addon of the group must be chosen. */
|
|
79
|
+
required: boolean().notNull().default(false),
|
|
80
|
+
/** When false (the default) at most one addon of the group can be chosen. */
|
|
81
|
+
multiple: boolean().notNull().default(false),
|
|
82
|
+
sortOrder: integer().notNull().default(0)
|
|
83
|
+
}, (t) => [
|
|
84
|
+
index("event_addon_group_ticket_idx").on(t.ticketTypeId)
|
|
85
|
+
]);
|
|
86
|
+
export const eventAddons = pgTable("event_addons", {
|
|
87
|
+
/** `adn_` + random id. */
|
|
88
|
+
id: text().primaryKey(),
|
|
89
|
+
groupId: text().notNull().references(() => eventAddonGroups.id, { onDelete: "cascade" }),
|
|
90
|
+
label: jsonb().$type().notNull(),
|
|
91
|
+
description: jsonb().$type(),
|
|
92
|
+
/** Price in minor units, in the ticket type's currency; `0` is included in the ticket. */
|
|
93
|
+
price: integer().notNull().default(0),
|
|
94
|
+
/** Maximum number of sold + reserved addons; null = unlimited. */
|
|
95
|
+
capacity: integer(),
|
|
96
|
+
sortOrder: integer().notNull().default(0)
|
|
97
|
+
}, (t) => [
|
|
98
|
+
index("event_addon_group_idx").on(t.groupId)
|
|
99
|
+
]);
|
|
100
|
+
export const eventTickets = pgTable("event_tickets", {
|
|
101
|
+
/** `tkt_` + random id. */
|
|
102
|
+
id: text().primaryKey(),
|
|
103
|
+
eventId: text().notNull(),
|
|
104
|
+
ticketTypeId: text().notNull(),
|
|
105
|
+
userId: integer().notNull(),
|
|
106
|
+
/** The payment that covers it; null for free tickets. */
|
|
107
|
+
paymentId: text(),
|
|
108
|
+
/** `pending | paid | cancelled | refunded | expired`. `pending` and `paid` hold a seat. */
|
|
109
|
+
status: text().notNull().default("pending"),
|
|
110
|
+
addons: jsonb().$type().notNull(),
|
|
111
|
+
amount: integer().notNull(),
|
|
112
|
+
currency: text().notNull(),
|
|
113
|
+
/** Unguessable value shown as a QR code and scanned at check-in. */
|
|
114
|
+
code: text().notNull().unique(),
|
|
115
|
+
/** Language the buyer was browsing in; used for mails and payment texts after the purchase. */
|
|
116
|
+
locale: text(),
|
|
117
|
+
checkedInAt: timestamp({ withTimezone: true }),
|
|
118
|
+
checkedInBy: integer(),
|
|
119
|
+
createdAt: timestamp({ withTimezone: true }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
120
|
+
updatedAt: timestamp({ withTimezone: true }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()).$onUpdateFn(() => /* @__PURE__ */ new Date())
|
|
121
|
+
}, (t) => [
|
|
122
|
+
index("event_ticket_event_idx").on(t.eventId, t.status),
|
|
123
|
+
index("event_ticket_user_idx").on(t.userId),
|
|
124
|
+
index("event_ticket_payment_idx").on(t.paymentId)
|
|
125
|
+
]);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type Priced = {
|
|
2
|
+
price?: number | null;
|
|
3
|
+
};
|
|
4
|
+
type TypeLike = Priced & {
|
|
5
|
+
currency?: string | null;
|
|
6
|
+
addonGroups?: Array<{
|
|
7
|
+
addons?: Priced[];
|
|
8
|
+
}>;
|
|
9
|
+
};
|
|
10
|
+
/** Output: a ticket type (DTO or row) with its price and add-on prices in whole units, currency upper-cased. */
|
|
11
|
+
export declare function ticketTypeToMajor<T extends TypeLike>(type: T): T;
|
|
12
|
+
/** Output: an event with `ticketTypes` converted by `ticketTypeToMajor`. */
|
|
13
|
+
export declare function eventToMajor<T extends {
|
|
14
|
+
ticketTypes: TypeLike[];
|
|
15
|
+
}>(event: T): T;
|
|
16
|
+
/** Output: a ticket (or attendee) with `amount` (and add-on prices) in whole units of its own currency. */
|
|
17
|
+
export declare function ticketToMajor<T extends {
|
|
18
|
+
amount: number;
|
|
19
|
+
currency: string;
|
|
20
|
+
addons?: Array<Priced & Record<string, unknown>>;
|
|
21
|
+
}>(ticket: T): T;
|
|
22
|
+
/** Input: a ticket type payload (agent, whole units) to minor units, using `fallbackCurrency` when it names none. */
|
|
23
|
+
export declare function ticketTypeToMinor<T extends TypeLike>(type: T, fallbackCurrency?: string): T;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { fromCents, toCents } from "@frigear-nu/mcp/utils";
|
|
2
|
+
import { DEFAULT_CURRENCY } from "../../utils/index.js";
|
|
3
|
+
const currencyOf = (type) => (type.currency ?? DEFAULT_CURRENCY).toUpperCase();
|
|
4
|
+
export function ticketTypeToMajor(type) {
|
|
5
|
+
const currency = currencyOf(type);
|
|
6
|
+
return {
|
|
7
|
+
...type,
|
|
8
|
+
...typeof type.price === "number" ? { price: fromCents(type.price, currency) } : {},
|
|
9
|
+
...type.currency ? { currency } : {},
|
|
10
|
+
...type.addonGroups ? {
|
|
11
|
+
addonGroups: type.addonGroups.map((group) => ({
|
|
12
|
+
...group,
|
|
13
|
+
...group.addons ? { addons: group.addons.map((addon) => typeof addon.price === "number" ? { ...addon, price: fromCents(addon.price, currency) } : addon) } : {}
|
|
14
|
+
}))
|
|
15
|
+
} : {}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function eventToMajor(event) {
|
|
19
|
+
return { ...event, ticketTypes: event.ticketTypes.map(ticketTypeToMajor) };
|
|
20
|
+
}
|
|
21
|
+
export function ticketToMajor(ticket) {
|
|
22
|
+
const currency = ticket.currency.toUpperCase();
|
|
23
|
+
return {
|
|
24
|
+
...ticket,
|
|
25
|
+
currency,
|
|
26
|
+
amount: fromCents(ticket.amount, currency),
|
|
27
|
+
...Array.isArray(ticket.addons) ? { addons: ticket.addons.map((addon) => typeof addon.price === "number" ? { ...addon, price: fromCents(addon.price, currency) } : addon) } : {}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function ticketTypeToMinor(type, fallbackCurrency = DEFAULT_CURRENCY) {
|
|
31
|
+
const currency = (type.currency ?? fallbackCurrency).toUpperCase();
|
|
32
|
+
return {
|
|
33
|
+
...type,
|
|
34
|
+
...typeof type.price === "number" ? { price: toCents(type.price, currency) } : {},
|
|
35
|
+
...type.addonGroups ? {
|
|
36
|
+
addonGroups: type.addonGroups.map((group) => ({
|
|
37
|
+
...group,
|
|
38
|
+
...group.addons ? { addons: group.addons.map((addon) => typeof addon.price === "number" ? { ...addon, price: toCents(addon.price, currency) } : addon) } : {}
|
|
39
|
+
}))
|
|
40
|
+
} : {}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { MONEY_NOTICE } from "@frigear-nu/mcp/utils";
|
|
2
3
|
const text = z.union([z.string().min(1), z.record(z.string(), z.string())]).describe('Text in one language, or a locale -> text map such as {"en": "Party", "da": "Fest"}.');
|
|
3
4
|
const membership = z.union([z.literal("any"), z.array(z.string())]).nullable().describe('"any", a list of plan keys, or null for no rule.');
|
|
4
5
|
const isoDate = z.string().describe("ISO 8601 date-time, e.g. 2026-12-24T18:00:00Z.");
|
|
5
6
|
const addon = z.object({
|
|
6
7
|
label: text,
|
|
7
8
|
description: text.nullish(),
|
|
8
|
-
price: z.number().
|
|
9
|
+
price: z.number().min(0).optional().describe(`${MONEY_NOTICE} In the ticket type's currency.`),
|
|
9
10
|
capacity: z.number().int().min(0).nullish(),
|
|
10
11
|
sortOrder: z.number().int().optional()
|
|
11
12
|
});
|
|
@@ -20,8 +21,8 @@ const ticketType = z.object({
|
|
|
20
21
|
key: z.string().min(1).max(64),
|
|
21
22
|
name: text,
|
|
22
23
|
description: text.nullish(),
|
|
23
|
-
price: z.number().
|
|
24
|
-
currency: z.string().length(3).optional(),
|
|
24
|
+
price: z.number().min(0).optional().describe(MONEY_NOTICE),
|
|
25
|
+
currency: z.string().length(3).optional().describe("ISO 4217 code such as DKK or USD. The price is in this currency; defaults to DKK for new ticket types."),
|
|
25
26
|
hidePrice: z.boolean().optional(),
|
|
26
27
|
capacity: z.number().int().min(0).nullish(),
|
|
27
28
|
saleStartsAt: isoDate.nullish(),
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createEventWithCatalog } from "../../../utils/admin.js";
|
|
2
2
|
import { eventInput } from "../../../utils/admin-schemas.js";
|
|
3
3
|
import { createEventShape } from "../../schemas.js";
|
|
4
|
+
import { ticketTypeToMinor } from "../../money.js";
|
|
4
5
|
export default defineScopedMcpTool({
|
|
5
|
-
description: "Create an event, optionally with ticket types, addon groups and addons (staff).",
|
|
6
|
+
description: "Create an event, optionally with ticket types, addon groups and addons (staff). Prices are in whole currency units (52.2 = 52.20), not cents.",
|
|
6
7
|
scopes: ["events:manage"],
|
|
7
8
|
gate: canManageEvents,
|
|
8
9
|
annotations: { destructiveHint: false },
|
|
9
10
|
inputSchema: createEventShape,
|
|
10
|
-
handler: (args) => createEventWithCatalog(eventInput.parse(args))
|
|
11
|
+
handler: (args) => createEventWithCatalog(eventInput.parse({ ...args, ticketTypes: args.ticketTypes?.map((type) => ticketTypeToMinor(type)) }))
|
|
11
12
|
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getEventWithTicketTypes } from "../../../utils/admin.js";
|
|
3
|
+
import { eventToMajor } from "../../money.js";
|
|
3
4
|
export default defineScopedMcpTool({
|
|
4
|
-
description: "Get one event with all its ticket types and their ids, whatever its status (staff). Use the ids with update-ticket-type.",
|
|
5
|
+
description: "Get one event with all its ticket types and their ids, whatever its status (staff). Use the ids with update-ticket-type. Prices are in whole currency units (52.2 = 52.20), not cents.",
|
|
5
6
|
scopes: ["events:manage"],
|
|
6
7
|
gate: canManageEvents,
|
|
7
8
|
annotations: { readOnlyHint: true },
|
|
8
9
|
inputSchema: { id: z.string().describe("The event id.") },
|
|
9
|
-
handler: ({ id }) => getEventWithTicketTypes(id)
|
|
10
|
+
handler: async ({ id }) => eventToMajor(await getEventWithTicketTypes(id))
|
|
10
11
|
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { db, schema } from "@nuxthub/db";
|
|
3
3
|
import { asc, eq } from "drizzle-orm";
|
|
4
|
+
import { ticketToMajor } from "../../money.js";
|
|
4
5
|
export default defineScopedMcpTool({
|
|
5
|
-
description: "List the tickets of an event (staff). Attendee names and emails are only included with the events:pii scope.",
|
|
6
|
+
description: "List the tickets of an event (staff). Attendee names and emails are only included with the events:pii scope. Amounts are in whole currency units (52.2 = 52.20), not cents.",
|
|
6
7
|
scopes: ["events:manage"],
|
|
7
8
|
gate: canManageEvents,
|
|
8
9
|
annotations: { readOnlyHint: true },
|
|
@@ -15,7 +16,7 @@ export default defineScopedMcpTool({
|
|
|
15
16
|
}).from(schema.eventTickets).leftJoin(schema.users, eq(schema.eventTickets.userId, schema.users.id)).where(eq(schema.eventTickets.eventId, eventId)).orderBy(asc(schema.eventTickets.createdAt));
|
|
16
17
|
const allow = ctx.hasScope("events:pii");
|
|
17
18
|
const attendees = rows.map(({ ticket, userName, userEmail }) => redactPii({
|
|
18
|
-
...pick(ticket, ["id", "ticketTypeId", "status", "amount", "currency", "checkedInAt", "createdAt"]),
|
|
19
|
+
...ticketToMajor(pick(ticket, ["id", "ticketTypeId", "status", "amount", "currency", "checkedInAt", "createdAt"])),
|
|
19
20
|
addons: ticket.addons.length,
|
|
20
21
|
userName,
|
|
21
22
|
userEmail
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
import { db, schema } from "@nuxthub/db";
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
1
3
|
import { patchTicketTypeRow, ticketTypePatchSchema } from "../../../utils/admin.js";
|
|
4
|
+
import { ticketTypeToMajor, ticketTypeToMinor } from "../../money.js";
|
|
2
5
|
import { updateTicketTypeShape } from "../../schemas.js";
|
|
3
6
|
export default defineScopedMcpTool({
|
|
4
|
-
description: "Update fields of an event's ticket type, such as price, capacity or sale dates (staff). Only the given fields change.",
|
|
7
|
+
description: "Update fields of an event's ticket type, such as price, capacity or sale dates (staff). Only the given fields change. The price is in whole currency units (52.2 = 52.20), not cents, in the given or the ticket type's current currency.",
|
|
5
8
|
scopes: ["events:manage"],
|
|
6
9
|
gate: canManageEvents,
|
|
7
10
|
annotations: { idempotentHint: true },
|
|
8
11
|
inputSchema: updateTicketTypeShape,
|
|
9
|
-
handler: ({ id, patch }) =>
|
|
12
|
+
handler: async ({ id, patch }) => {
|
|
13
|
+
const [existing] = patch.price === void 0 || patch.currency ? [void 0] : await db.select({ currency: schema.eventTicketTypes.currency }).from(schema.eventTicketTypes).where(eq(schema.eventTicketTypes.id, id)).limit(1);
|
|
14
|
+
const row = await patchTicketTypeRow(id, ticketTypePatchSchema.parse(ticketTypeToMinor(patch, existing?.currency)));
|
|
15
|
+
return ticketTypeToMajor(row);
|
|
16
|
+
}
|
|
10
17
|
});
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { eventToMajor } from "../money.js";
|
|
2
3
|
export default defineScopedMcpTool({
|
|
3
|
-
description: "Get a published event with its ticket types and what you can buy.",
|
|
4
|
+
description: "Get a published event with its ticket types and what you can buy. Prices are in whole currency units (52.2 = 52.20), not cents.",
|
|
4
5
|
scopes: ["tickets:read"],
|
|
5
6
|
annotations: { readOnlyHint: true },
|
|
6
7
|
inputSchema: { slug: z.string().describe("The event slug.") },
|
|
7
8
|
handler: async ({ slug }, { user }) => {
|
|
8
9
|
const row = await getPublishedEventRow(slug);
|
|
9
10
|
if (!row) throw createError({ statusCode: 404, statusMessage: "Event not found." });
|
|
10
|
-
return getEventDetail(row, user.id);
|
|
11
|
+
return eventToMajor(await getEventDetail(row, user.id));
|
|
11
12
|
}
|
|
12
13
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { ticketToMajor } from "../money.js";
|
|
1
2
|
export default defineScopedMcpTool({
|
|
2
|
-
description: "List your own tickets.",
|
|
3
|
+
description: "List your own tickets. Amounts and prices are in whole currency units (52.2 = 52.20), not cents.",
|
|
3
4
|
scopes: ["tickets:read"],
|
|
4
5
|
annotations: { readOnlyHint: true },
|
|
5
|
-
handler: (_args, { user }) => listUserTickets(user.id)
|
|
6
|
+
handler: async (_args, { user }) => (await listUserTickets(user.id)).map(ticketToMajor)
|
|
6
7
|
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { db, schema } from "@nuxthub/db";
|
|
2
|
+
import { and, asc, count, eq, gte } from "drizzle-orm";
|
|
3
|
+
const LIMIT = 5;
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
await requireEventsManager(event);
|
|
6
|
+
const upcoming = and(eq(schema.events.status, "published"), gte(schema.events.startsAt, /* @__PURE__ */ new Date()));
|
|
7
|
+
const [[total], items] = await Promise.all([
|
|
8
|
+
db.select({ value: count() }).from(schema.events).where(upcoming),
|
|
9
|
+
db.select({
|
|
10
|
+
id: schema.events.id,
|
|
11
|
+
name: schema.events.name,
|
|
12
|
+
location: schema.events.location,
|
|
13
|
+
startsAt: schema.events.startsAt
|
|
14
|
+
}).from(schema.events).where(upcoming).orderBy(asc(schema.events.startsAt)).limit(LIMIT)
|
|
15
|
+
]);
|
|
16
|
+
return { total: total?.value ?? 0, items };
|
|
17
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frigear-nu/events",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
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
5
|
"homepage": "https://docs.frigear.dev/modules/events",
|
|
6
6
|
"repository": {
|
|
@@ -30,15 +30,15 @@
|
|
|
30
30
|
"playground"
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@frigear-nu/admin": "0.0.
|
|
34
|
-
"@frigear-nu/module-kit": "0.1
|
|
35
|
-
"@frigear-nu/payments": "0.0.
|
|
36
|
-
"@frigear-nu/stripe": "0.0.
|
|
37
|
-
"@frigear-nu/subscriptions": "0.0.
|
|
38
|
-
"@frigear-nu/ui": "0.0.
|
|
39
|
-
"@frigear-nu/auth": "0.0.
|
|
40
|
-
"@frigear-nu/mcp": "0.0.
|
|
41
|
-
"@frigear-nu/users": "1.0.
|
|
33
|
+
"@frigear-nu/admin": "0.0.4",
|
|
34
|
+
"@frigear-nu/module-kit": "1.0.1",
|
|
35
|
+
"@frigear-nu/payments": "0.0.10",
|
|
36
|
+
"@frigear-nu/stripe": "0.0.10",
|
|
37
|
+
"@frigear-nu/subscriptions": "0.0.10",
|
|
38
|
+
"@frigear-nu/ui": "0.0.9",
|
|
39
|
+
"@frigear-nu/auth": "0.0.5",
|
|
40
|
+
"@frigear-nu/mcp": "0.0.5",
|
|
41
|
+
"@frigear-nu/users": "1.0.11",
|
|
42
42
|
"@libsql/client": "^0.18.0",
|
|
43
43
|
"@nuxt/kit": "^4.6.0",
|
|
44
44
|
"@vueuse/core": "^15.0.0",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"zod": "^4.6.5"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@frigear-nu/menu": "0.0.
|
|
55
|
-
"@frigear-nu/widget": "0.0.
|
|
54
|
+
"@frigear-nu/menu": "0.0.11",
|
|
55
|
+
"@frigear-nu/widget": "0.0.4",
|
|
56
56
|
"@nuxt/ui": "^4.10.0",
|
|
57
57
|
"@nuxthub/core": "^0.10.8"
|
|
58
58
|
},
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"@frigear-nu/menu": "0.0.
|
|
72
|
-
"@frigear-nu/widget": "0.0.
|
|
71
|
+
"@frigear-nu/menu": "0.0.11",
|
|
72
|
+
"@frigear-nu/widget": "0.0.4",
|
|
73
73
|
"@nuxt/devtools": "^3.4.2",
|
|
74
74
|
"@nuxt/eslint-config": "^1.17.0",
|
|
75
75
|
"@nuxt/module-builder": "^1.0.3",
|