@getmicdrop/venue-calendar 3.3.1 → 3.3.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/dist/{VenueCalendar-Xppig0q_.js → VenueCalendar-BMSfRl2d.js} +10 -13
- package/dist/{VenueCalendar-Xppig0q_.js.map → VenueCalendar-BMSfRl2d.js.map} +1 -1
- package/dist/api/api.cjs +2 -0
- package/dist/api/api.cjs.map +1 -0
- package/dist/api/api.mjs +787 -0
- package/dist/api/api.mjs.map +1 -0
- package/dist/api/client.d.ts +46 -0
- package/dist/api/events.d.ts +102 -0
- package/dist/api/index.d.ts +38 -0
- package/dist/api/orders.d.ts +104 -0
- package/dist/api/promo.d.ts +45 -0
- package/dist/api/transformers/event.d.ts +86 -0
- package/dist/api/transformers/index.d.ts +9 -0
- package/dist/api/transformers/order.d.ts +105 -0
- package/dist/api/transformers/venue.d.ts +48 -0
- package/dist/api/venues.d.ts +33 -0
- package/dist/{index-BjErG0CG.js → index-CoJaem3n.js} +2 -2
- package/dist/{index-BjErG0CG.js.map → index-CoJaem3n.js.map} +1 -1
- package/dist/venue-calendar.css +1 -1
- package/dist/venue-calendar.es.js +1 -1
- package/dist/venue-calendar.iife.js +4 -4
- package/dist/venue-calendar.iife.js.map +1 -1
- package/dist/venue-calendar.umd.js +4 -4
- package/dist/venue-calendar.umd.js.map +1 -1
- package/package.json +96 -94
- package/src/lib/api/client.ts +0 -210
- package/src/lib/api/events.ts +0 -358
- package/src/lib/api/index.ts +0 -182
- package/src/lib/api/orders.ts +0 -390
- package/src/lib/api/promo.ts +0 -164
- package/src/lib/api/transformers/event.ts +0 -248
- package/src/lib/api/transformers/index.ts +0 -29
- package/src/lib/api/transformers/order.ts +0 -207
- package/src/lib/api/transformers/venue.ts +0 -118
- package/src/lib/api/venues.ts +0 -100
- package/src/lib/utils/api.js +0 -790
- package/src/lib/utils/api.test.js +0 -1284
- package/src/lib/utils/constants.js +0 -8
- package/src/lib/utils/constants.test.js +0 -39
- package/src/lib/utils/datetime.js +0 -266
- package/src/lib/utils/datetime.test.js +0 -340
- package/src/lib/utils/event-transform.js +0 -464
- package/src/lib/utils/event-transform.test.js +0 -413
- package/src/lib/utils/logger.js +0 -105
- package/src/lib/utils/timezone.js +0 -109
- package/src/lib/utils/timezone.test.js +0 -222
- package/src/lib/utils/utils.js +0 -806
- package/src/lib/utils/utils.test.js +0 -959
- /package/{src/lib/api/types.ts → dist/api/types.d.ts} +0 -0
package/src/lib/utils/api.js
DELETED
|
@@ -1,790 +0,0 @@
|
|
|
1
|
-
import { logger } from '$lib/utils/logger.js';
|
|
2
|
-
|
|
3
|
-
const API_BASE_URL = "https://get-micdrop.com";
|
|
4
|
-
const PUBLIC_BASE_URL = `${API_BASE_URL}/api/v2/public`;
|
|
5
|
-
|
|
6
|
-
// Note: Stripe publishable key is now fetched dynamically from the payment intent API response
|
|
7
|
-
// This ensures third-party embeds get the correct key without needing environment variables
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Create a payment intent for the cart
|
|
11
|
-
* @param {string} cartId - The cart/order UUID
|
|
12
|
-
* @param {Object} quantities - Map of ticketId -> quantity
|
|
13
|
-
* @returns {Promise<{clientSecret: string, ...} | null>}
|
|
14
|
-
*/
|
|
15
|
-
export async function createPaymentIntent(cartId, quantities) {
|
|
16
|
-
try {
|
|
17
|
-
// Get IP address
|
|
18
|
-
const ipRes = await fetch('https://api.ipify.org?format=json');
|
|
19
|
-
const ipData = await ipRes.json();
|
|
20
|
-
const ip = ipData.ip;
|
|
21
|
-
|
|
22
|
-
const res = await fetch(
|
|
23
|
-
`${PUBLIC_BASE_URL}/orders/${cartId}/payment-intent`,
|
|
24
|
-
{
|
|
25
|
-
method: 'POST',
|
|
26
|
-
headers: {
|
|
27
|
-
'Content-Type': 'application/json',
|
|
28
|
-
},
|
|
29
|
-
credentials: 'include',
|
|
30
|
-
body: JSON.stringify({
|
|
31
|
-
IP: ip,
|
|
32
|
-
productQuantities: quantities,
|
|
33
|
-
}),
|
|
34
|
-
}
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
if (!res.ok) {
|
|
38
|
-
const errorData = await res.json().catch(() => ({}));
|
|
39
|
-
logger.error('Payment intent creation failed:', errorData);
|
|
40
|
-
throw new Error(errorData.error || 'Failed to create payment intent');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const data = await res.json();
|
|
44
|
-
logger.debug('create payment intent API response:', data);
|
|
45
|
-
return data;
|
|
46
|
-
} catch (err) {
|
|
47
|
-
logger.error('createPaymentIntent error:', err);
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Map human-readable timezone strings to IANA timezone identifiers
|
|
53
|
-
function getIANATimezone(tzString) {
|
|
54
|
-
if (!tzString || tzString === 'NaN') return 'America/Los_Angeles'; // Default to LA
|
|
55
|
-
|
|
56
|
-
const tzMap = {
|
|
57
|
-
'UTC': 'UTC',
|
|
58
|
-
'California USA': 'America/Los_Angeles',
|
|
59
|
-
'California': 'America/Los_Angeles',
|
|
60
|
-
'Los Angeles': 'America/Los_Angeles',
|
|
61
|
-
'New York': 'America/New_York',
|
|
62
|
-
'New York USA': 'America/New_York',
|
|
63
|
-
'Chicago': 'America/Chicago',
|
|
64
|
-
'Chicago USA': 'America/Chicago',
|
|
65
|
-
'Denver': 'America/Denver',
|
|
66
|
-
'Denver USA': 'America/Denver',
|
|
67
|
-
'Phoenix': 'America/Phoenix',
|
|
68
|
-
'Phoenix USA': 'America/Phoenix',
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// Check direct match first
|
|
72
|
-
if (tzMap[tzString]) return tzMap[tzString];
|
|
73
|
-
|
|
74
|
-
// Check if it's already an IANA timezone
|
|
75
|
-
try {
|
|
76
|
-
Intl.DateTimeFormat(undefined, { timeZone: tzString });
|
|
77
|
-
return tzString;
|
|
78
|
-
} catch (e) {
|
|
79
|
-
// Not a valid IANA timezone, default to LA
|
|
80
|
-
return 'America/Los_Angeles';
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export async function fetchAllVenues(orgId) {
|
|
85
|
-
try {
|
|
86
|
-
if (!orgId) {
|
|
87
|
-
logger.warn("fetchAllVenues called without orgId");
|
|
88
|
-
return [];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const response = await fetch(`${PUBLIC_BASE_URL}/venues/organization/${orgId}`);
|
|
92
|
-
|
|
93
|
-
if (!response.ok) {
|
|
94
|
-
logger.error(`Failed to fetch venues: ${response.status} ${response.statusText}`);
|
|
95
|
-
return [];
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const venues = await response.json();
|
|
99
|
-
logger.debug("Venues:", venues);
|
|
100
|
-
// Ensure we always return an array, even if API returns null/undefined
|
|
101
|
-
return Array.isArray(venues) ? venues : [];
|
|
102
|
-
} catch (error) {
|
|
103
|
-
logger.error("Error fetching venues:", error);
|
|
104
|
-
return [];
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export async function fetchVenueEvents(venueId) {
|
|
109
|
-
try {
|
|
110
|
-
if (!venueId) {
|
|
111
|
-
logger.warn("fetchVenueEvents called without venueId");
|
|
112
|
-
return [];
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const response = await fetch(`${PUBLIC_BASE_URL}/events/venue/${venueId}`);
|
|
116
|
-
|
|
117
|
-
if (!response.ok) {
|
|
118
|
-
logger.error(`Failed to fetch events: ${response.status} ${response.statusText}`);
|
|
119
|
-
return [];
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const events = await response.json();
|
|
123
|
-
// Ensure we always return an array, even if API returns null/undefined
|
|
124
|
-
return Array.isArray(events) ? events : [];
|
|
125
|
-
} catch (error) {
|
|
126
|
-
// Network errors, CORS issues, etc. - return empty array instead of throwing
|
|
127
|
-
logger.error("Error fetching events:", error);
|
|
128
|
-
return [];
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Fetch events for a specific month (paginated API)
|
|
134
|
-
* @param {number|string} venueId - Venue ID
|
|
135
|
-
* @param {number} year - Year (e.g., 2025)
|
|
136
|
-
* @param {number} month - Month (1-12, NOT 0-indexed)
|
|
137
|
-
* @returns {Promise<{events: Array, pagination: {hasPrevMonth: boolean, hasNextMonth: boolean, prevMonth: {year, month}, nextMonth: {year, month}}}>}
|
|
138
|
-
*/
|
|
139
|
-
export async function getMonthEvents(venueId, year, month) {
|
|
140
|
-
try {
|
|
141
|
-
if (!venueId) {
|
|
142
|
-
logger.warn("getMonthEvents called without venueId");
|
|
143
|
-
return { events: [], pagination: null };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const response = await fetch(`${PUBLIC_BASE_URL}/events/venue/${venueId}/month/${year}/${month}`);
|
|
147
|
-
|
|
148
|
-
if (!response.ok) {
|
|
149
|
-
logger.error(`Failed to fetch month events: ${response.status} ${response.statusText}`);
|
|
150
|
-
return { events: [], pagination: null };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const data = await response.json();
|
|
154
|
-
return {
|
|
155
|
-
events: Array.isArray(data.events) ? data.events : [],
|
|
156
|
-
pagination: data.pagination || null,
|
|
157
|
-
year: data.year,
|
|
158
|
-
month: data.month
|
|
159
|
-
};
|
|
160
|
-
} catch (error) {
|
|
161
|
-
logger.error("Error fetching month events:", error);
|
|
162
|
-
return { events: [], pagination: null };
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Fetch events for a specific month at org level (all venues under an org)
|
|
168
|
-
* @param {number|string} orgId - Organization ID (userID of org owner)
|
|
169
|
-
* @param {number} year - Year (e.g., 2025)
|
|
170
|
-
* @param {number} month - Month (1-12, NOT 0-indexed)
|
|
171
|
-
* @returns {Promise<{events: Array, pagination: object}>}
|
|
172
|
-
*/
|
|
173
|
-
export async function getOrgMonthEvents(orgId, year, month) {
|
|
174
|
-
try {
|
|
175
|
-
if (!orgId) {
|
|
176
|
-
logger.warn("getOrgMonthEvents called without orgId");
|
|
177
|
-
return { events: [], pagination: null };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const response = await fetch(`${PUBLIC_BASE_URL}/events/organization/${orgId}/month/${year}/${month}`);
|
|
181
|
-
|
|
182
|
-
if (!response.ok) {
|
|
183
|
-
logger.error(`Failed to fetch org month events: ${response.status} ${response.statusText}`);
|
|
184
|
-
return { events: [], pagination: null };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const data = await response.json();
|
|
188
|
-
return {
|
|
189
|
-
events: Array.isArray(data.events) ? data.events : [],
|
|
190
|
-
pagination: data.pagination || null,
|
|
191
|
-
year: data.year,
|
|
192
|
-
month: data.month
|
|
193
|
-
};
|
|
194
|
-
} catch (error) {
|
|
195
|
-
logger.error("Error fetching org month events:", error);
|
|
196
|
-
return { events: [], pagination: null };
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export function transformEventData(apiEvent) {
|
|
201
|
-
// Get the venue's timezone (API returns strings like "California USA" or "UTC")
|
|
202
|
-
const venueTimezone = getIANATimezone(apiEvent.timeZone);
|
|
203
|
-
|
|
204
|
-
// Extract date from startDateTime in the venue's timezone
|
|
205
|
-
// API returns UTC like "2025-11-30T20:00:00Z" which should display in venue TZ
|
|
206
|
-
let startDate = '';
|
|
207
|
-
if (apiEvent.startDateTime) {
|
|
208
|
-
const dt = new Date(apiEvent.startDateTime);
|
|
209
|
-
// Format date in the venue's timezone
|
|
210
|
-
const dateInVenueTZ = dt.toLocaleDateString('en-CA', {
|
|
211
|
-
timeZone: venueTimezone,
|
|
212
|
-
year: 'numeric',
|
|
213
|
-
month: '2-digit',
|
|
214
|
-
day: '2-digit'
|
|
215
|
-
});
|
|
216
|
-
// en-CA gives YYYY-MM-DD format
|
|
217
|
-
startDate = dateInVenueTZ;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Handle image URLs - prepend DigitalOcean Spaces CDN URL for relative paths
|
|
221
|
-
let imageUrl = apiEvent.image || apiEvent.imageUrl || apiEvent.image_url || '';
|
|
222
|
-
if (imageUrl && imageUrl.startsWith('/')) {
|
|
223
|
-
imageUrl = `https://moxy.sfo3.digitaloceanspaces.com${imageUrl}`;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Calculate scarcity data from available tickets if present
|
|
227
|
-
// The API may include availableTickets in the list response
|
|
228
|
-
const tickets = apiEvent.availableTickets || [];
|
|
229
|
-
const publicTickets = tickets.filter(t => t.salesChannel !== 2); // Exclude "At the door only"
|
|
230
|
-
|
|
231
|
-
let totalRemaining = 0;
|
|
232
|
-
let totalCapacity = 0;
|
|
233
|
-
let isSoldOut = false;
|
|
234
|
-
|
|
235
|
-
if (publicTickets.length > 0) {
|
|
236
|
-
for (const ticket of publicTickets) {
|
|
237
|
-
const remaining = ticket.remainingCapacity ?? ticket.quantityRemaining ?? ticket.quantity ?? 0;
|
|
238
|
-
const capacity = ticket.totalCapacity ?? ticket.quantity ?? remaining;
|
|
239
|
-
totalRemaining += remaining;
|
|
240
|
-
totalCapacity += capacity;
|
|
241
|
-
}
|
|
242
|
-
// Check if all tickets are sold out
|
|
243
|
-
isSoldOut = totalRemaining === 0;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const transformed = {
|
|
247
|
-
id: apiEvent.id,
|
|
248
|
-
name: apiEvent.title || apiEvent.name || '',
|
|
249
|
-
date: startDate || apiEvent.date || apiEvent.startDate || apiEvent.eventDate || '',
|
|
250
|
-
image: imageUrl,
|
|
251
|
-
status: apiEvent.status || 'On Sale',
|
|
252
|
-
timeline: formatEventTime(apiEvent, venueTimezone),
|
|
253
|
-
description: apiEvent.description || apiEvent.eventSummary || '',
|
|
254
|
-
venueId: apiEvent.venueId || apiEvent.venue_id,
|
|
255
|
-
timeZone: venueTimezone,
|
|
256
|
-
// Series info for grouping recurring events
|
|
257
|
-
eventSeriesId: apiEvent.eventSeriesId || null,
|
|
258
|
-
// Keep raw startDateTime for sorting
|
|
259
|
-
startDateTime: apiEvent.startDateTime || null,
|
|
260
|
-
// Scarcity data for browse views
|
|
261
|
-
ticketsRemaining: totalRemaining,
|
|
262
|
-
ticketsTotal: totalCapacity,
|
|
263
|
-
isSoldOut: isSoldOut,
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
return transformed;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function formatEventTime(event, venueTimezone = 'America/Los_Angeles') {
|
|
270
|
-
// Format time from API datetime strings in the venue's timezone
|
|
271
|
-
const formatTime = (dateTimeStr) => {
|
|
272
|
-
if (!dateTimeStr) return '';
|
|
273
|
-
try {
|
|
274
|
-
const date = new Date(dateTimeStr);
|
|
275
|
-
return date.toLocaleTimeString('en-US', {
|
|
276
|
-
timeZone: venueTimezone,
|
|
277
|
-
hour: 'numeric',
|
|
278
|
-
minute: '2-digit',
|
|
279
|
-
hour12: true
|
|
280
|
-
});
|
|
281
|
-
} catch (e) {
|
|
282
|
-
return '';
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
const startTime = formatTime(event.startDateTime);
|
|
287
|
-
const endTime = formatTime(event.endDateTime);
|
|
288
|
-
|
|
289
|
-
if (startTime && endTime) {
|
|
290
|
-
return `${startTime} - ${endTime}`;
|
|
291
|
-
}
|
|
292
|
-
if (startTime) {
|
|
293
|
-
return startTime;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Fallback to old field names if new ones don't exist
|
|
297
|
-
if (event.startTime && event.endTime) {
|
|
298
|
-
return `${event.startTime} - ${event.endTime}`;
|
|
299
|
-
}
|
|
300
|
-
if (event.startTime) {
|
|
301
|
-
return event.startTime;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return '';
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export async function fetchEventDetails(eventId, customFetch = fetch) {
|
|
308
|
-
try {
|
|
309
|
-
const response = await customFetch(`${PUBLIC_BASE_URL}/events/${eventId}`);
|
|
310
|
-
|
|
311
|
-
if (!response.ok) {
|
|
312
|
-
throw new Error(`Failed to fetch event details: ${response.status}`);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const event = await response.json();
|
|
316
|
-
return event;
|
|
317
|
-
} catch (error) {
|
|
318
|
-
logger.error("Error fetching event details:", error);
|
|
319
|
-
throw error;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
export async function fetchEventTickets(eventId) {
|
|
324
|
-
try {
|
|
325
|
-
const response = await fetch(`${PUBLIC_BASE_URL}/tickets/event/${eventId}`);
|
|
326
|
-
|
|
327
|
-
if (!response.ok) {
|
|
328
|
-
throw new Error(`Failed to fetch tickets: ${response.status}`);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const tickets = await response.json();
|
|
332
|
-
return Array.isArray(tickets) ? tickets : [];
|
|
333
|
-
} catch (error) {
|
|
334
|
-
logger.error("Error fetching tickets:", error);
|
|
335
|
-
throw error;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Validate a promo code for an event via the public API
|
|
341
|
-
* @param {string} eventId - The event ID
|
|
342
|
-
* @param {string} code - The promo code to validate
|
|
343
|
-
* @returns {Promise<{valid: boolean, revealHiddenTickets?: boolean, revealTicketIds?: number[], provideDiscount?: boolean, discountType?: string, amount?: number}>}
|
|
344
|
-
*/
|
|
345
|
-
export async function validatePromoCode(eventId, code) {
|
|
346
|
-
try {
|
|
347
|
-
// API uses GET with code in URL path: /validatePromoCode/{eventId}/{code}
|
|
348
|
-
const encodedCode = encodeURIComponent(code);
|
|
349
|
-
const response = await fetch(`${PUBLIC_BASE_URL}/promo-codes/validate/${eventId}/${encodedCode}`);
|
|
350
|
-
|
|
351
|
-
if (!response.ok) {
|
|
352
|
-
// Invalid code or server error
|
|
353
|
-
return { valid: false };
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const result = await response.json();
|
|
357
|
-
return result;
|
|
358
|
-
} catch (error) {
|
|
359
|
-
logger.error("Error validating promo code:", error);
|
|
360
|
-
return { valid: false };
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Check if promo codes are available for an event
|
|
366
|
-
* @param {string} eventId - The event ID
|
|
367
|
-
* @returns {Promise<boolean>} - Whether promo codes exist for this event
|
|
368
|
-
*/
|
|
369
|
-
export async function hasPromoCodes(eventId) {
|
|
370
|
-
try {
|
|
371
|
-
const response = await fetch(`${PUBLIC_BASE_URL}/promo-codes/check/${eventId}`);
|
|
372
|
-
|
|
373
|
-
if (!response.ok) {
|
|
374
|
-
// If endpoint doesn't exist or errors, default to showing the input
|
|
375
|
-
return true;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const result = await response.json();
|
|
379
|
-
return result.hasPromoCodes === true;
|
|
380
|
-
} catch (error) {
|
|
381
|
-
logger.error("Error checking promo codes availability:", error);
|
|
382
|
-
// Default to showing promo input if we can't check
|
|
383
|
-
return true;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Extend the checkout session by 15 minutes
|
|
389
|
-
* @param {string} orderUuid - The order UUID
|
|
390
|
-
* @returns {Promise<{success: boolean, newExpiryTime?: string, remainingExtensions?: number, error?: string}>}
|
|
391
|
-
*/
|
|
392
|
-
export async function extendCheckoutSession(orderUuid) {
|
|
393
|
-
try {
|
|
394
|
-
const response = await fetch(`${PUBLIC_BASE_URL}/orders/extend-session`, {
|
|
395
|
-
method: 'POST',
|
|
396
|
-
headers: {
|
|
397
|
-
'Content-Type': 'application/json',
|
|
398
|
-
},
|
|
399
|
-
body: JSON.stringify({ orderUuid }),
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
if (!response.ok) {
|
|
403
|
-
const errorData = await response.json().catch(() => ({}));
|
|
404
|
-
return {
|
|
405
|
-
success: false,
|
|
406
|
-
error: errorData.error || 'Failed to extend session'
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const result = await response.json();
|
|
411
|
-
return {
|
|
412
|
-
success: true,
|
|
413
|
-
newExpiryTime: result.newExpiryTime,
|
|
414
|
-
remainingExtensions: result.remainingExtensions,
|
|
415
|
-
};
|
|
416
|
-
} catch (error) {
|
|
417
|
-
logger.error("Error extending checkout session:", error);
|
|
418
|
-
return { success: false, error: 'Network error extending session' };
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* Get current session status including expiry time
|
|
424
|
-
* @param {string} orderUuid - The order UUID
|
|
425
|
-
* @returns {Promise<{expiresAt?: string, extensionCount?: number, remainingExtensions?: number, canExtend?: boolean, error?: string}>}
|
|
426
|
-
*/
|
|
427
|
-
export async function getSessionStatus(orderUuid) {
|
|
428
|
-
try {
|
|
429
|
-
const response = await fetch(`${PUBLIC_BASE_URL}/orders/session/${orderUuid}`);
|
|
430
|
-
|
|
431
|
-
if (!response.ok) {
|
|
432
|
-
const errorData = await response.json().catch(() => ({}));
|
|
433
|
-
return { error: errorData.error || 'No active session found' };
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const result = await response.json();
|
|
437
|
-
return {
|
|
438
|
-
expiresAt: result.expiresAt,
|
|
439
|
-
extensionCount: result.extensionCount,
|
|
440
|
-
remainingExtensions: result.remainingExtensions,
|
|
441
|
-
canExtend: result.canExtend,
|
|
442
|
-
reservationCount: result.reservationCount,
|
|
443
|
-
};
|
|
444
|
-
} catch (error) {
|
|
445
|
-
logger.error("Error getting session status:", error);
|
|
446
|
-
return { error: 'Network error getting session status' };
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Complete reservation after successful payment
|
|
452
|
-
* @param {string} orderUuid - The order UUID
|
|
453
|
-
* @returns {Promise<{success: boolean, message?: string, error?: string}>}
|
|
454
|
-
*/
|
|
455
|
-
export async function completeReservation(orderUuid) {
|
|
456
|
-
try {
|
|
457
|
-
const response = await fetch(`${PUBLIC_BASE_URL}/orders/complete/${orderUuid}`, {
|
|
458
|
-
method: 'POST',
|
|
459
|
-
headers: {
|
|
460
|
-
'Content-Type': 'application/json',
|
|
461
|
-
},
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
if (!response.ok) {
|
|
465
|
-
const errorData = await response.json().catch(() => ({}));
|
|
466
|
-
return {
|
|
467
|
-
success: false,
|
|
468
|
-
error: errorData.error || 'Failed to complete reservation'
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const result = await response.json();
|
|
473
|
-
return {
|
|
474
|
-
success: true,
|
|
475
|
-
message: result.message,
|
|
476
|
-
};
|
|
477
|
-
} catch (error) {
|
|
478
|
-
logger.error("Error completing reservation:", error);
|
|
479
|
-
return { success: false, error: 'Network error completing reservation' };
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* Cancel reservation and release tickets back to inventory
|
|
485
|
-
* @param {string} orderUuid - The order UUID
|
|
486
|
-
* @returns {Promise<{success: boolean, message?: string, error?: string}>}
|
|
487
|
-
*/
|
|
488
|
-
export async function cancelReservation(orderUuid) {
|
|
489
|
-
try {
|
|
490
|
-
const response = await fetch(`${PUBLIC_BASE_URL}/orders/cancel/${orderUuid}`, {
|
|
491
|
-
method: 'POST',
|
|
492
|
-
headers: {
|
|
493
|
-
'Content-Type': 'application/json',
|
|
494
|
-
},
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
if (!response.ok) {
|
|
498
|
-
const errorData = await response.json().catch(() => ({}));
|
|
499
|
-
return {
|
|
500
|
-
success: false,
|
|
501
|
-
error: errorData.error || 'Failed to cancel reservation'
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
const result = await response.json();
|
|
506
|
-
return {
|
|
507
|
-
success: true,
|
|
508
|
-
message: result.message,
|
|
509
|
-
};
|
|
510
|
-
} catch (error) {
|
|
511
|
-
logger.error("Error cancelling reservation:", error);
|
|
512
|
-
return { success: false, error: 'Network error cancelling reservation' };
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Fetch performers for a public event
|
|
518
|
-
* Uses the new public API endpoint that returns pre-resolved avatar URLs
|
|
519
|
-
* @param {string|number} eventId - The event ID
|
|
520
|
-
* @returns {Promise<{performers: Array<{id: number, stageName: string, displayName: string, avatar: string, order: number}>, showPerformers: boolean}>}
|
|
521
|
-
*/
|
|
522
|
-
export async function fetchEventPerformers(eventId) {
|
|
523
|
-
try {
|
|
524
|
-
if (!eventId) {
|
|
525
|
-
logger.warn("fetchEventPerformers called without eventId");
|
|
526
|
-
return { performers: [], showPerformers: false };
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
const response = await fetch(`${PUBLIC_BASE_URL}/events/${eventId}/performers`);
|
|
530
|
-
|
|
531
|
-
if (!response.ok) {
|
|
532
|
-
logger.error(`Failed to fetch performers: ${response.status} ${response.statusText}`);
|
|
533
|
-
return { performers: [], showPerformers: false };
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
const data = await response.json();
|
|
537
|
-
return {
|
|
538
|
-
performers: Array.isArray(data.performers) ? data.performers : [],
|
|
539
|
-
showPerformers: data.showPerformers === true,
|
|
540
|
-
};
|
|
541
|
-
} catch (error) {
|
|
542
|
-
logger.error("Error fetching event performers:", error);
|
|
543
|
-
return { performers: [], showPerformers: false };
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* Get series occurrences for date selector
|
|
549
|
-
* @param {number} eventSeriesId - The event series ID
|
|
550
|
-
* @returns {Promise<{seriesId?: number, seriesName?: string, occurrences?: Array, error?: string}>}
|
|
551
|
-
*/
|
|
552
|
-
export async function getSeriesOccurrences(eventSeriesId) {
|
|
553
|
-
try {
|
|
554
|
-
const response = await fetch(`${PUBLIC_BASE_URL}/series/${eventSeriesId}/occurrences`);
|
|
555
|
-
|
|
556
|
-
if (!response.ok) {
|
|
557
|
-
const errorData = await response.json().catch(() => ({}));
|
|
558
|
-
return { error: errorData.error || 'Event series not found' };
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const result = await response.json();
|
|
562
|
-
return {
|
|
563
|
-
seriesId: result.seriesId,
|
|
564
|
-
seriesName: result.seriesName,
|
|
565
|
-
occurrences: result.occurrences || [],
|
|
566
|
-
};
|
|
567
|
-
} catch (error) {
|
|
568
|
-
logger.error("Error fetching series occurrences:", error);
|
|
569
|
-
return { error: 'Network error fetching series occurrences' };
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* Check if a password is correct for a password-protected event
|
|
575
|
-
* @param {string} eventId - The event ID
|
|
576
|
-
* @param {string} password - The password attempt
|
|
577
|
-
* @returns {Promise<{valid: boolean}>}
|
|
578
|
-
*/
|
|
579
|
-
export async function checkEventPassword(eventId, password) {
|
|
580
|
-
try {
|
|
581
|
-
const encodedPassword = encodeURIComponent(password);
|
|
582
|
-
const response = await fetch(`${PUBLIC_BASE_URL}/events/${eventId}/check-password/${encodedPassword}`);
|
|
583
|
-
|
|
584
|
-
if (!response.ok) {
|
|
585
|
-
return { valid: false };
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
const result = await response.json();
|
|
589
|
-
// API may return boolean directly or object with valid property
|
|
590
|
-
if (typeof result === 'boolean') {
|
|
591
|
-
return { valid: result };
|
|
592
|
-
}
|
|
593
|
-
return { valid: result.valid === true || result === true };
|
|
594
|
-
} catch (error) {
|
|
595
|
-
logger.error("Error checking event password:", error);
|
|
596
|
-
return { valid: false };
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
export async function testNetworkConnection(orgId = null, venueId = null) {
|
|
601
|
-
const results = {
|
|
602
|
-
organization: false,
|
|
603
|
-
venue: false,
|
|
604
|
-
timestamp: new Date().toISOString()
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
if (orgId) {
|
|
608
|
-
try {
|
|
609
|
-
const orgResponse = await fetch(`${PUBLIC_BASE_URL}/venues/organization/${orgId}`, {
|
|
610
|
-
method: 'GET',
|
|
611
|
-
signal: AbortSignal.timeout(5000)
|
|
612
|
-
});
|
|
613
|
-
results.organization = orgResponse.ok;
|
|
614
|
-
} catch (error) {
|
|
615
|
-
logger.error("Organization API test failed:", error);
|
|
616
|
-
results.organization = false;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
if (venueId) {
|
|
621
|
-
try {
|
|
622
|
-
const venueResponse = await fetch(`${PUBLIC_BASE_URL}/events/venue/${venueId}`, {
|
|
623
|
-
method: 'GET',
|
|
624
|
-
signal: AbortSignal.timeout(5000)
|
|
625
|
-
});
|
|
626
|
-
results.venue = venueResponse.ok;
|
|
627
|
-
} catch (error) {
|
|
628
|
-
logger.error("Venue API test failed:", error);
|
|
629
|
-
results.venue = false;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
return results;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
/**
|
|
637
|
-
* Compute CTA button state for an event based on its tickets
|
|
638
|
-
* @param {Object} event - The event object with startDateTime, endDateTime
|
|
639
|
-
* @param {Array} tickets - Array of ticket objects
|
|
640
|
-
* @returns {{text: string, disabled: boolean, reason: string}}
|
|
641
|
-
*/
|
|
642
|
-
function computeCtaState(event, tickets) {
|
|
643
|
-
const now = new Date();
|
|
644
|
-
|
|
645
|
-
// Check if event is in the past
|
|
646
|
-
const eventEnd = event.endDateTime || event.startDateTime;
|
|
647
|
-
if (eventEnd && new Date(eventEnd) < now) {
|
|
648
|
-
return { text: 'Sales ended', disabled: true, reason: 'event_past' };
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// Filter to public tickets (exclude "At the door only" - salesChannel = 2)
|
|
652
|
-
const publicTickets = (tickets || []).filter(t => t.salesChannel !== 2);
|
|
653
|
-
|
|
654
|
-
// No public tickets available
|
|
655
|
-
if (publicTickets.length === 0) {
|
|
656
|
-
return { text: 'No tickets', disabled: true, reason: 'no_tickets' };
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// Check ticket statuses
|
|
660
|
-
let allSoldOut = true;
|
|
661
|
-
let allSalesEnded = true;
|
|
662
|
-
let allComingSoon = true;
|
|
663
|
-
let earliestSalesStart = null;
|
|
664
|
-
|
|
665
|
-
for (const ticket of publicTickets) {
|
|
666
|
-
const salesBegin = ticket.salesBegin || ticket.salesStart || ticket.saleBegin || ticket.onSaleStart;
|
|
667
|
-
const salesEnd = ticket.salesEnd || ticket.saleEnd || ticket.onSaleEnd;
|
|
668
|
-
const remaining = ticket.remainingCapacity ?? ticket.quantityRemaining ?? ticket.quantity;
|
|
669
|
-
const isSoldOut = ticket.soldOut || (remaining !== null && remaining !== undefined && remaining <= 0);
|
|
670
|
-
|
|
671
|
-
const isScheduled = salesBegin && new Date(salesBegin) > now;
|
|
672
|
-
const hasSalesEnded = salesEnd && new Date(salesEnd) < now;
|
|
673
|
-
|
|
674
|
-
if (!isSoldOut) allSoldOut = false;
|
|
675
|
-
if (!hasSalesEnded) allSalesEnded = false;
|
|
676
|
-
if (!isScheduled) allComingSoon = false;
|
|
677
|
-
|
|
678
|
-
if (isScheduled && salesBegin) {
|
|
679
|
-
const startDate = new Date(salesBegin);
|
|
680
|
-
if (!earliestSalesStart || startDate < earliestSalesStart) {
|
|
681
|
-
earliestSalesStart = startDate;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
if (allSalesEnded) {
|
|
687
|
-
return { text: 'Sales ended', disabled: true, reason: 'sales_ended' };
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
if (allSoldOut) {
|
|
691
|
-
return { text: 'Sold out', disabled: true, reason: 'sold_out' };
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
if (allComingSoon && earliestSalesStart) {
|
|
695
|
-
// Always show month + day + time format: "Dec 19 12:04 PM"
|
|
696
|
-
const dateText = earliestSalesStart.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
697
|
-
const timeText = earliestSalesStart.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
|
698
|
-
return { text: `On sale ${dateText} ${timeText}`, disabled: true, reason: 'coming_soon' };
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// Default: tickets available
|
|
702
|
-
return { text: 'Get tickets', disabled: false, reason: 'available' };
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
/**
|
|
706
|
-
* Fetch all occurrences of an event series with CTA state
|
|
707
|
-
* Uses the optimized getSeriesOccurrences API first, falls back to listAllEvents if needed
|
|
708
|
-
* @param {number} eventSeriesId - The event series ID
|
|
709
|
-
* @param {number} venueId - The venue ID to fetch events from (used for fallback)
|
|
710
|
-
* @returns {Promise<Array<{id: number, startDateTime: string, slug: string, name: string, ctaState: {text: string, disabled: boolean, reason: string}}>>}
|
|
711
|
-
*/
|
|
712
|
-
export async function fetchSeriesOccurrences(eventSeriesId, venueId) {
|
|
713
|
-
if (!eventSeriesId || eventSeriesId <= 0) {
|
|
714
|
-
return [];
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
try {
|
|
718
|
-
// Try the new optimized API first
|
|
719
|
-
const seriesResult = await getSeriesOccurrences(eventSeriesId);
|
|
720
|
-
|
|
721
|
-
if (!seriesResult.error && seriesResult.occurrences?.length > 0) {
|
|
722
|
-
// Map API response to expected format
|
|
723
|
-
// The API returns: { id, slug, startDateTime, endDateTime, ctaState }
|
|
724
|
-
return seriesResult.occurrences
|
|
725
|
-
.map(occ => ({
|
|
726
|
-
id: occ.id,
|
|
727
|
-
name: occ.name || occ.title || '',
|
|
728
|
-
slug: occ.slug,
|
|
729
|
-
startDateTime: occ.startDateTime,
|
|
730
|
-
endDateTime: occ.endDateTime,
|
|
731
|
-
ctaState: occ.ctaState || computeCtaState(occ, occ.availableTickets || [])
|
|
732
|
-
}))
|
|
733
|
-
.sort((a, b) => new Date(a.startDateTime) - new Date(b.startDateTime));
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
// Fallback to the old method if new API fails or returns empty
|
|
737
|
-
if (!venueId) {
|
|
738
|
-
return [];
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// Use existing listAllEvents API and filter by series ID
|
|
742
|
-
const response = await fetch(`${PUBLIC_BASE_URL}/events/venue/${venueId}`);
|
|
743
|
-
|
|
744
|
-
if (!response.ok) {
|
|
745
|
-
throw new Error(`Failed to fetch venue events: ${response.status}`);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
const allEvents = await response.json();
|
|
749
|
-
|
|
750
|
-
// Filter events that belong to this series
|
|
751
|
-
const seriesEvents = allEvents.filter(e => e.eventSeriesId === eventSeriesId);
|
|
752
|
-
|
|
753
|
-
// For each event in the series, fetch full details (tickets are included in availableTickets)
|
|
754
|
-
const occurrences = await Promise.all(
|
|
755
|
-
seriesEvents.map(async (event) => {
|
|
756
|
-
try {
|
|
757
|
-
const eventResponse = await fetch(`${PUBLIC_BASE_URL}/events/${event.id}`);
|
|
758
|
-
if (!eventResponse.ok) return null;
|
|
759
|
-
|
|
760
|
-
const fullEvent = await eventResponse.json();
|
|
761
|
-
// Tickets are included in the event response as availableTickets
|
|
762
|
-
const tickets = fullEvent.availableTickets || [];
|
|
763
|
-
|
|
764
|
-
// Compute CTA state based on event and ticket data
|
|
765
|
-
const ctaState = computeCtaState(fullEvent, tickets);
|
|
766
|
-
|
|
767
|
-
return {
|
|
768
|
-
id: fullEvent.id,
|
|
769
|
-
name: fullEvent.name,
|
|
770
|
-
slug: fullEvent.slug,
|
|
771
|
-
startDateTime: fullEvent.startDateTime,
|
|
772
|
-
endDateTime: fullEvent.endDateTime,
|
|
773
|
-
ctaState
|
|
774
|
-
};
|
|
775
|
-
} catch {
|
|
776
|
-
return null;
|
|
777
|
-
}
|
|
778
|
-
})
|
|
779
|
-
);
|
|
780
|
-
|
|
781
|
-
// Filter out nulls and sort by start date
|
|
782
|
-
return occurrences
|
|
783
|
-
.filter(Boolean)
|
|
784
|
-
.sort((a, b) => new Date(a.startDateTime) - new Date(b.startDateTime));
|
|
785
|
-
} catch (error) {
|
|
786
|
-
logger.error("Error fetching series occurrences:", error);
|
|
787
|
-
return [];
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
|