@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.
Files changed (49) hide show
  1. package/dist/{VenueCalendar-Xppig0q_.js → VenueCalendar-BMSfRl2d.js} +10 -13
  2. package/dist/{VenueCalendar-Xppig0q_.js.map → VenueCalendar-BMSfRl2d.js.map} +1 -1
  3. package/dist/api/api.cjs +2 -0
  4. package/dist/api/api.cjs.map +1 -0
  5. package/dist/api/api.mjs +787 -0
  6. package/dist/api/api.mjs.map +1 -0
  7. package/dist/api/client.d.ts +46 -0
  8. package/dist/api/events.d.ts +102 -0
  9. package/dist/api/index.d.ts +38 -0
  10. package/dist/api/orders.d.ts +104 -0
  11. package/dist/api/promo.d.ts +45 -0
  12. package/dist/api/transformers/event.d.ts +86 -0
  13. package/dist/api/transformers/index.d.ts +9 -0
  14. package/dist/api/transformers/order.d.ts +105 -0
  15. package/dist/api/transformers/venue.d.ts +48 -0
  16. package/dist/api/venues.d.ts +33 -0
  17. package/dist/{index-BjErG0CG.js → index-CoJaem3n.js} +2 -2
  18. package/dist/{index-BjErG0CG.js.map → index-CoJaem3n.js.map} +1 -1
  19. package/dist/venue-calendar.css +1 -1
  20. package/dist/venue-calendar.es.js +1 -1
  21. package/dist/venue-calendar.iife.js +4 -4
  22. package/dist/venue-calendar.iife.js.map +1 -1
  23. package/dist/venue-calendar.umd.js +4 -4
  24. package/dist/venue-calendar.umd.js.map +1 -1
  25. package/package.json +96 -94
  26. package/src/lib/api/client.ts +0 -210
  27. package/src/lib/api/events.ts +0 -358
  28. package/src/lib/api/index.ts +0 -182
  29. package/src/lib/api/orders.ts +0 -390
  30. package/src/lib/api/promo.ts +0 -164
  31. package/src/lib/api/transformers/event.ts +0 -248
  32. package/src/lib/api/transformers/index.ts +0 -29
  33. package/src/lib/api/transformers/order.ts +0 -207
  34. package/src/lib/api/transformers/venue.ts +0 -118
  35. package/src/lib/api/venues.ts +0 -100
  36. package/src/lib/utils/api.js +0 -790
  37. package/src/lib/utils/api.test.js +0 -1284
  38. package/src/lib/utils/constants.js +0 -8
  39. package/src/lib/utils/constants.test.js +0 -39
  40. package/src/lib/utils/datetime.js +0 -266
  41. package/src/lib/utils/datetime.test.js +0 -340
  42. package/src/lib/utils/event-transform.js +0 -464
  43. package/src/lib/utils/event-transform.test.js +0 -413
  44. package/src/lib/utils/logger.js +0 -105
  45. package/src/lib/utils/timezone.js +0 -109
  46. package/src/lib/utils/timezone.test.js +0 -222
  47. package/src/lib/utils/utils.js +0 -806
  48. package/src/lib/utils/utils.test.js +0 -959
  49. /package/{src/lib/api/types.ts → dist/api/types.d.ts} +0 -0
@@ -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
-