@getmicdrop/venue-calendar 3.3.0 → 3.3.1

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 (39) hide show
  1. package/README.md +661 -661
  2. package/dist/{VenueCalendar-BMSfRl2d.js → VenueCalendar-Xppig0q_.js} +13 -10
  3. package/dist/VenueCalendar-Xppig0q_.js.map +1 -0
  4. package/dist/{index-CoJaem3n.js → index-BjErG0CG.js} +2 -2
  5. package/dist/{index-CoJaem3n.js.map → index-BjErG0CG.js.map} +1 -1
  6. package/dist/types/index.d.ts +395 -395
  7. package/dist/venue-calendar.css +1 -1
  8. package/dist/venue-calendar.es.js +1 -1
  9. package/dist/venue-calendar.iife.js +4 -4
  10. package/dist/venue-calendar.iife.js.map +1 -1
  11. package/dist/venue-calendar.umd.js +4 -4
  12. package/dist/venue-calendar.umd.js.map +1 -1
  13. package/package.json +2 -1
  14. package/src/lib/api/client.ts +210 -210
  15. package/src/lib/api/events.ts +358 -358
  16. package/src/lib/api/index.ts +182 -182
  17. package/src/lib/api/orders.ts +390 -390
  18. package/src/lib/api/promo.ts +164 -164
  19. package/src/lib/api/transformers/event.ts +248 -248
  20. package/src/lib/api/transformers/index.ts +29 -29
  21. package/src/lib/api/transformers/order.ts +207 -207
  22. package/src/lib/api/transformers/venue.ts +118 -118
  23. package/src/lib/api/types.ts +289 -289
  24. package/src/lib/api/venues.ts +100 -100
  25. package/src/lib/theme.js +209 -209
  26. package/src/lib/utils/api.js +790 -0
  27. package/src/lib/utils/api.test.js +1284 -0
  28. package/src/lib/utils/constants.js +8 -0
  29. package/src/lib/utils/constants.test.js +39 -0
  30. package/src/lib/utils/datetime.js +266 -0
  31. package/src/lib/utils/datetime.test.js +340 -0
  32. package/src/lib/utils/event-transform.js +464 -0
  33. package/src/lib/utils/event-transform.test.js +413 -0
  34. package/src/lib/utils/logger.js +105 -0
  35. package/src/lib/utils/timezone.js +109 -0
  36. package/src/lib/utils/timezone.test.js +222 -0
  37. package/src/lib/utils/utils.js +806 -0
  38. package/src/lib/utils/utils.test.js +959 -0
  39. package/dist/VenueCalendar-BMSfRl2d.js.map +0 -1
@@ -0,0 +1,806 @@
1
+ import Cookies from "js-cookie";
2
+ import {
3
+ transformEvent as transformEventNew,
4
+ TransformMode,
5
+ calculateTicketStatus as calculateTicketStatusNew,
6
+ findLowestPrice as findLowestPriceNew,
7
+ } from './event-transform.js';
8
+
9
+ // Re-export datetime functions from consolidated module
10
+ export {
11
+ getIANATimezone,
12
+ formatEventTime,
13
+ formatTimeRange,
14
+ formatCleanTimeRange,
15
+ formatDayOfWeek,
16
+ formatMonth,
17
+ formatHour,
18
+ getDateParts,
19
+ formatEventDateTime,
20
+ formatNotificationTime,
21
+ isToday,
22
+ DEFAULT_TIMEZONE,
23
+ } from './datetime.js';
24
+
25
+ // Re-export from consolidated module for backward compatibility
26
+ export { transformEventNew as transformEvent };
27
+ export { calculateTicketStatusNew as calculateTicketStatus };
28
+ export { findLowestPriceNew as findLowestPrice };
29
+
30
+ /**
31
+ * Get the number of days in a month for a given date
32
+ * @param {Date} date - Any date in the target month
33
+ * @returns {number} - Number of days in that month (28-31)
34
+ */
35
+ export function getDaysInMonth(date) {
36
+ return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
37
+ }
38
+
39
+ /**
40
+ * Navigation utility that works in both SvelteKit and standalone environments
41
+ * @param {string} url - The URL to navigate to
42
+ * @param {Object} options - Navigation options
43
+ * @param {boolean} options.replaceState - Whether to replace current history entry
44
+ * @param {Function} options.navigateFn - Optional custom navigation function (for SvelteKit)
45
+ */
46
+ export function navigate(url, options = {}) {
47
+ const { replaceState = false, navigateFn } = options;
48
+
49
+ // Use custom navigation function if provided (for SvelteKit)
50
+ if (navigateFn && typeof navigateFn === 'function') {
51
+ navigateFn(url, { replaceState });
52
+ return;
53
+ }
54
+
55
+ // Standalone environment - use browser navigation
56
+ if (typeof window !== 'undefined') {
57
+ if (replaceState) {
58
+ window.history.replaceState({}, '', url);
59
+ } else {
60
+ window.history.pushState({}, '', url);
61
+ }
62
+
63
+ // Dispatch a custom event for standalone environments to handle navigation
64
+ window.dispatchEvent(new CustomEvent('micdrop-navigate', { detail: { url, replaceState } }));
65
+ }
66
+ }
67
+
68
+ export function classNames(...classes) {
69
+ return classes.filter(Boolean).join(" ");
70
+ }
71
+
72
+ export function truncateTitle(title, maxLength) {
73
+ if (title.length > maxLength) {
74
+ return title.slice(0, maxLength) + "...";
75
+ }
76
+ return title;
77
+ }
78
+
79
+ export function getDays(params) {
80
+ if (params === "weeks") {
81
+ return [
82
+ { value: "monday", label: "Monday" },
83
+ { value: "tuesday", label: "Tuesday" },
84
+ { value: "wednesday", label: "Wednesday" },
85
+ { value: "thursday", label: "Thursday" },
86
+ { value: "friday", label: "Friday" },
87
+ ];
88
+ } else {
89
+ return Array.from({ length: 31 }, (_, i) => {
90
+ const day = i + 1;
91
+ const suffix =
92
+ day % 10 === 1 && day !== 11
93
+ ? "st"
94
+ : day % 10 === 2 && day !== 12
95
+ ? "nd"
96
+ : day % 10 === 3 && day !== 13
97
+ ? "rd"
98
+ : "th";
99
+ return {
100
+ value: day.toString(),
101
+ label: day + suffix,
102
+ };
103
+ });
104
+ }
105
+ }
106
+
107
+ export function getDaysNumberOptions(timeUnit) {
108
+ if (timeUnit === "months") {
109
+ return Array.from({ length: 12 }, (_, i) => ({
110
+ value: (i + 1).toString(),
111
+ label: (i + 1).toString(),
112
+ }));
113
+ } else if (timeUnit === "weeks") {
114
+ return Array.from({ length: 10 }, (_, i) => ({
115
+ value: (i + 1).toString(),
116
+ label: (i + 1).toString(),
117
+ }));
118
+ } else {
119
+ return Array.from({ length: 31 }, (_, i) => ({
120
+ value: (i + 1).toString(),
121
+ label: (i + 1).toString(),
122
+ }));
123
+ }
124
+ }
125
+
126
+ // =============================================================================
127
+ // DEPRECATED: Date formatting functions
128
+ // These are now re-exported from datetime.js (see top of file).
129
+ // The functions below are kept temporarily for any legacy code that imports them
130
+ // directly. They will be removed in a future version.
131
+ // =============================================================================
132
+
133
+ /**
134
+ * @deprecated Use formatEventDateTime from datetime.js instead
135
+ */
136
+ export function convertToDate(value, timeZone = "UTC") {
137
+ if (!value) return null;
138
+
139
+ const date = new Date(value);
140
+ return date.toLocaleString("en-US", {
141
+ weekday: "short",
142
+ month: "short",
143
+ day: "numeric",
144
+ year: "numeric",
145
+ hour: "numeric",
146
+ minute: "2-digit",
147
+ hour12: true,
148
+ timeZone: timeZone,
149
+ });
150
+ }
151
+
152
+ /**
153
+ * @deprecated This is a legacy date conversion function
154
+ */
155
+ export function convertToCustomDateTimeFormat(isoDateString) {
156
+ const date = new Date(isoDateString);
157
+ return new Date(
158
+ Date.UTC(
159
+ date.getUTCFullYear(),
160
+ date.getUTCMonth(),
161
+ date.getUTCDate(),
162
+ date.getUTCHours(),
163
+ date.getUTCMinutes()
164
+ )
165
+ );
166
+ }
167
+
168
+ /**
169
+ * @deprecated Use formatEventTime from datetime.js instead
170
+ */
171
+ export const formatTimeline = (startDateTime, timeZone = "UTC") => {
172
+ if (!startDateTime) return "";
173
+
174
+ const date = new Date(startDateTime);
175
+ if (isNaN(date.getTime())) return "";
176
+
177
+ return new Intl.DateTimeFormat("en-US", {
178
+ hour: "numeric",
179
+ minute: "2-digit",
180
+ hour12: true,
181
+ timeZone: timeZone,
182
+ }).format(date);
183
+ };
184
+
185
+ /**
186
+ * @deprecated Use formatDayOfWeek from datetime.js instead
187
+ */
188
+ export const getDay = (dateString, timeZone = "UTC") => {
189
+ if (!dateString) return "";
190
+
191
+ const date = new Date(dateString);
192
+ if (isNaN(date.getTime())) return "";
193
+
194
+ return new Intl.DateTimeFormat("en-US", {
195
+ weekday: "short",
196
+ timeZone: timeZone,
197
+ }).format(date);
198
+ };
199
+
200
+ /**
201
+ * @deprecated Use getDateParts from datetime.js instead
202
+ */
203
+ export const getMonth = (dateString, timeZone = "UTC") => {
204
+ if (!dateString) return "";
205
+
206
+ const date = new Date(dateString);
207
+ if (isNaN(date.getTime())) return "";
208
+
209
+ return new Intl.DateTimeFormat("en-US", {
210
+ month: "short",
211
+ timeZone: timeZone,
212
+ }).format(date);
213
+ };
214
+
215
+ /**
216
+ * @deprecated Use getDateParts from datetime.js instead
217
+ */
218
+ export const getDateOfMonth = (dateString, timeZone = "UTC") => {
219
+ if (!dateString) return "";
220
+
221
+ const date = new Date(dateString);
222
+ if (isNaN(date.getTime())) return "";
223
+
224
+ return new Intl.DateTimeFormat("en-US", {
225
+ day: "numeric",
226
+ timeZone: timeZone,
227
+ }).format(date);
228
+ };
229
+
230
+ export function resize(node, callback) {
231
+ const updateDimensions = () => {
232
+ callback({
233
+ width: window.innerWidth,
234
+ height: window.innerHeight,
235
+ });
236
+ };
237
+
238
+ updateDimensions();
239
+ window.addEventListener("resize", updateDimensions);
240
+
241
+ return {
242
+ destroy() {
243
+ window.removeEventListener("resize", updateDimensions);
244
+ },
245
+ };
246
+ }
247
+
248
+ export function convertToCustomDateFormat(isoString) {
249
+ const date = new Date(isoString);
250
+
251
+ const year = date.getUTCFullYear();
252
+ const month = date.getUTCMonth();
253
+ const day = date.getUTCDate();
254
+
255
+ const customDate = new Date(year, month, day);
256
+
257
+ return customDate;
258
+ }
259
+
260
+ export function validateNegativeNumber(value) {
261
+ if (value === null || value === undefined || value === "") {
262
+ return "Must be greater than zero.";
263
+ }
264
+
265
+ const stringValue = String(value).trim();
266
+
267
+ if (
268
+ stringValue.includes("--") ||
269
+ stringValue.includes("++") ||
270
+ stringValue.match(/^-+$/) ||
271
+ stringValue.match(/^\++$/)
272
+ ) {
273
+ return "Please enter a valid number";
274
+ }
275
+
276
+ if (stringValue.match(/^-?\d+-$/)) {
277
+ return "Please enter a valid number";
278
+ }
279
+
280
+ if ((stringValue.match(/\./g) || []).length > 1) {
281
+ return "Please enter a valid number";
282
+ }
283
+
284
+ const numValue = Number(value);
285
+ if (isNaN(numValue)) {
286
+ return "Please enter a valid number";
287
+ }
288
+
289
+ if (numValue < 0 || numValue === 0) {
290
+ return "Price must be greater than zero.";
291
+ }
292
+
293
+ return "";
294
+ }
295
+ /**
296
+ * @deprecated Use formatEventDateTime and formatEventTime from datetime.js instead
297
+ */
298
+ export function formatDateTimeWithDoors(startDateTime, doorsOpenTime, timeZone = "UTC") {
299
+ if (!startDateTime) return "";
300
+
301
+ const date = new Date(startDateTime);
302
+ const options = {
303
+ weekday: "short",
304
+ year: "numeric",
305
+ month: "short",
306
+ day: "numeric",
307
+ hour: "numeric",
308
+ minute: "2-digit",
309
+ hour12: true,
310
+ timeZone: timeZone,
311
+ };
312
+
313
+ const formattedDateTime = new Intl.DateTimeFormat("en-US", options).format(
314
+ date
315
+ );
316
+ if (doorsOpenTime) {
317
+ const doorsDate = new Date(doorsOpenTime);
318
+ const doorsFormatted = new Intl.DateTimeFormat("en-US", {
319
+ hour: "numeric",
320
+ minute: "2-digit",
321
+ hour12: true,
322
+ timeZone: timeZone,
323
+ }).format(doorsDate);
324
+
325
+ return `${formattedDateTime} (Doors: ${doorsFormatted})`;
326
+ }
327
+
328
+ return formattedDateTime;
329
+ }
330
+
331
+ export async function getClientIP() {
332
+ try {
333
+ const res = await fetch("https://api.ipify.org?format=json");
334
+ const { ip } = await res.json();
335
+ return ip;
336
+ } catch (err) {
337
+ console.error("Failed to fetch IP:", err);
338
+ return null;
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Generate a URL-friendly slug from a string
344
+ * @param {string} text - The text to slugify
345
+ * @returns {string} - A URL-safe slug
346
+ */
347
+ export function generateSlug(text) {
348
+ if (!text) return "";
349
+
350
+ return text
351
+ .toLowerCase()
352
+ .trim()
353
+ .replace(/[^\w\s-]/g, "")
354
+ .replace(/\s+/g, "-")
355
+ .replace(/--+/g, "-")
356
+ .replace(/^-+|-+$/g, "");
357
+ }
358
+
359
+ // calculateTicketStatus - now re-exported from event-transform.js at top of file
360
+
361
+ // findLowestPrice - now re-exported from event-transform.js at top of file
362
+
363
+ export function getImageUrl(
364
+ imagePath,
365
+ baseUrl = "https://moxy.sfo3.cdn.digitaloceanspaces.com"
366
+ ) {
367
+ if (!imagePath) return "";
368
+
369
+ if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
370
+ return imagePath;
371
+ }
372
+
373
+ return `${baseUrl}${imagePath}`;
374
+ }
375
+
376
+ /**
377
+ * Get the display avatar path for a performer based on their profileImageSource setting.
378
+ * This mirrors the backend logic in auth/utils/avatar.go
379
+ * @param {Object} profile - The performer profile object
380
+ * @returns {string} - The avatar path (relative or absolute URL)
381
+ */
382
+ export function getDisplayAvatarPath(profile) {
383
+ if (!profile) return "";
384
+
385
+ const source = profile.profileImageSource || "1";
386
+
387
+ // Map source number to the corresponding avatar field
388
+ switch (source) {
389
+ case "1":
390
+ return profile.avatarPosition1 || profile.profileImage || "";
391
+ case "2":
392
+ return profile.avatarPosition2 || "";
393
+ case "3":
394
+ return profile.avatarPosition3 || "";
395
+ case "4":
396
+ return profile.avatarPosition4 || "";
397
+ case "5":
398
+ return profile.avatarPosition5 || "";
399
+ case "6":
400
+ return profile.avatarPosition6 || "";
401
+ default:
402
+ // Fallback to position 1 or profileImage
403
+ return profile.avatarPosition1 || profile.profileImage || "";
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Get the full display avatar URL for a performer.
409
+ * Combines getDisplayAvatarPath with CDN URL resolution.
410
+ * @param {Object} profile - The performer profile object
411
+ * @param {string} baseUrl - CDN base URL (default: DigitalOcean Spaces)
412
+ * @returns {string} - The full avatar URL
413
+ */
414
+ export function getDisplayAvatarUrl(profile, baseUrl = "https://moxy.sfo3.cdn.digitaloceanspaces.com") {
415
+ const path = getDisplayAvatarPath(profile);
416
+ if (!path) return "";
417
+ return getImageUrl(path, baseUrl);
418
+ }
419
+
420
+ /**
421
+ * Get the performer display name based on their profile settings.
422
+ * This mirrors the backend logic in auth/utils/avatar.go
423
+ * @param {Object} profile - The performer profile object
424
+ * @returns {string} - The display name
425
+ */
426
+ export function getPerformerDisplayName(profile) {
427
+ if (!profile) return "";
428
+
429
+ // If useStageName is set and stageName exists, use it
430
+ if (profile.useStageName && profile.stageName) {
431
+ return profile.stageName;
432
+ }
433
+
434
+ // Otherwise use first + last name
435
+ const firstName = profile.firstName || "";
436
+ const lastName = profile.lastName || "";
437
+ const fullName = `${firstName} ${lastName}`.trim();
438
+
439
+ // Fallback to stageName if no real name
440
+ return fullName || profile.stageName || "";
441
+ }
442
+
443
+ // transformEvent - now re-exported from event-transform.js at top of file
444
+
445
+
446
+
447
+ export async function getVenueDetails(venueID) {
448
+ try {
449
+ const response = await fetch(
450
+ `https://get-micdrop.com/api/v2/public/venues/${venueID}`
451
+ );
452
+
453
+ if (!response.ok) {
454
+ throw new Error(`API error: ${response.status}`);
455
+ }
456
+
457
+ const data = await response.json();
458
+ return data;
459
+ } catch (error) {
460
+ console.error("getVenueDetails error:", error);
461
+ throw error;
462
+ }
463
+ }
464
+
465
+ export function setCookie(name, value, options = {}) {
466
+ Cookies.set(name, value, options);
467
+ }
468
+
469
+ export function getCookie(name) {
470
+ return Cookies.get(name);
471
+ }
472
+
473
+ export function removeCookie(name) {
474
+ Cookies.remove(name);
475
+ }
476
+
477
+ /**
478
+ * Load checkout state from localStorage (preferred) with cookie fallback for migration
479
+ * @param {string} eventID - The event ID
480
+ * @returns {Object} - Checkout state with quantities, promocode, promoDiscountAmount, tickets
481
+ */
482
+ export function loadCheckoutStateFromCookies(eventID) {
483
+ let quantities = {};
484
+ let donationAmounts = {};
485
+ let tickets = [];
486
+ let promocode = "";
487
+ let promoDiscountAmount = 0;
488
+
489
+ if (typeof window === 'undefined') {
490
+ return { quantities, donationAmounts, promocode, promoDiscountAmount, tickets };
491
+ }
492
+
493
+ // Try localStorage first (new approach)
494
+ try {
495
+ const stored = localStorage.getItem(`checkout-state-${eventID}`);
496
+ if (stored) {
497
+ const parsed = JSON.parse(stored);
498
+ return {
499
+ quantities: parsed.quantities || {},
500
+ donationAmounts: parsed.donationAmounts || {},
501
+ promocode: parsed.promocode || "",
502
+ promoDiscountAmount: parseFloat(parsed.promoDiscountAmount) || 0,
503
+ tickets: parsed.tickets || [],
504
+ };
505
+ }
506
+ } catch (e) {
507
+ console.warn("Failed to parse checkout state from localStorage:", e);
508
+ }
509
+
510
+ // Fallback to cookies for migration (old approach)
511
+ try {
512
+ quantities = JSON.parse(getCookie(`checkout-quantities-${eventID}`) || "{}");
513
+ } catch (e) {
514
+ console.warn("Failed to parse checkout quantities cookie:", e);
515
+ quantities = {};
516
+ }
517
+
518
+ try {
519
+ tickets = JSON.parse(getCookie(`checkout-tickets-${eventID}`) || "[]");
520
+ } catch (e) {
521
+ console.warn("Failed to parse checkout tickets cookie:", e);
522
+ tickets = [];
523
+ }
524
+
525
+ promocode = getCookie(`checkout-promocode-${eventID}`) || "";
526
+ promoDiscountAmount = parseFloat(getCookie("checkout-promo-discount") || 0);
527
+
528
+ // If we found data in cookies, migrate it to localStorage and clear cookies
529
+ if (Object.keys(quantities).length > 0 || tickets.length > 0 || promocode) {
530
+ persistCheckoutState({ eventID, quantities, donationAmounts, promocode, promoDiscountAmount, tickets });
531
+ clearCheckoutCookies(eventID);
532
+ }
533
+
534
+ return { quantities, donationAmounts, promocode, promoDiscountAmount, tickets };
535
+ }
536
+
537
+ /**
538
+ * Clear checkout state from both localStorage and cookies
539
+ * @param {string} eventID - The event ID
540
+ */
541
+ export function clearCheckoutCookies(eventID) {
542
+ // Clear localStorage (new approach)
543
+ if (typeof window !== 'undefined') {
544
+ localStorage.removeItem(`checkout-state-${eventID}`);
545
+ }
546
+
547
+ // Clear cookies (for migration cleanup)
548
+ removeCookie(`checkout-quantities-${eventID}`);
549
+ removeCookie(`checkout-promocode-${eventID}`);
550
+ removeCookie(`checkout-promo-discount`);
551
+ removeCookie(`checkout-tickets-${eventID}`);
552
+ }
553
+
554
+ /**
555
+ * Persist checkout state to localStorage
556
+ * Uses localStorage instead of cookies to avoid size limits and reduce request overhead
557
+ * @param {Object} params - Checkout state parameters
558
+ * @param {string} params.eventID - The event ID
559
+ * @param {Object} params.quantities - Ticket quantities by ID
560
+ * @param {string} params.promocode - Applied promo code
561
+ * @param {number} params.promoDiscountAmount - Discount amount
562
+ * @param {Array} params.tickets - Selected tickets (minimal data only)
563
+ */
564
+ export function persistCheckoutState({
565
+ eventID,
566
+ quantities,
567
+ donationAmounts,
568
+ promocode,
569
+ promoDiscountAmount,
570
+ tickets,
571
+ }) {
572
+ if (typeof window === 'undefined') return;
573
+
574
+ // Store minimal ticket data to avoid storage bloat
575
+ const minimalTickets = (tickets || []).map(t => ({
576
+ ID: t.ID,
577
+ name: t.name,
578
+ price: t.price,
579
+ type: t.type, // Include type for donation ticket detection
580
+ visibility: t.visibility,
581
+ salesChannel: t.salesChannel,
582
+ }));
583
+
584
+ const state = {
585
+ quantities: quantities || {},
586
+ donationAmounts: donationAmounts || {},
587
+ promocode: promocode || "",
588
+ promoDiscountAmount: promoDiscountAmount || 0,
589
+ tickets: minimalTickets,
590
+ updatedAt: Date.now(),
591
+ };
592
+
593
+ try {
594
+ localStorage.setItem(`checkout-state-${eventID}`, JSON.stringify(state));
595
+ } catch (e) {
596
+ console.warn("Failed to persist checkout state to localStorage:", e);
597
+ }
598
+ }
599
+
600
+ /**
601
+ * Ensure order ID exists, create if not
602
+ * @param {string} eventId - The event ID
603
+ * @returns {Promise<string>} - Order ID
604
+ */
605
+ export async function ensureOrderIdExists(eventId) {
606
+ const existingOrderId = getCookie(`order-id-${eventId}`);
607
+
608
+ if (existingOrderId) {
609
+ return existingOrderId;
610
+ }
611
+
612
+ const newOrderId = `order-${eventId}-${Date.now()}`;
613
+ setCookie(`order-id-${eventId}`, newOrderId);
614
+
615
+ return newOrderId;
616
+ }
617
+
618
+ // Note: Parameter is named venueID for backwards compatibility but actually represents eventID
619
+ export async function trackUTMSource(venueID) {
620
+ const urlParams = new URLSearchParams(window.location.search);
621
+ const utmSource = urlParams.get("utm_source") || "Direct";
622
+
623
+ try {
624
+ await fetch(
625
+ `https://get-micdrop.com/api/v2/public/utm/${venueID}/${utmSource}`
626
+ );
627
+ } catch (err) {
628
+ console.error("UTM tracking failed:", err);
629
+ }
630
+ }
631
+
632
+ export async function initiateOrder(cartData = {}) {
633
+ try {
634
+ const ip = await getClientIP();
635
+
636
+ // Note: donationAmounts is not yet supported by backend API
637
+ // TODO: Remove this filter once Gus adds backend support for donationAmounts
638
+ const { donationAmounts, eventID, quantities, ...restCartData } = cartData;
639
+
640
+ // Convert quantities keys to numbers and eventID to number (API expects numeric types)
641
+ const numericQuantities = quantities ? Object.fromEntries(
642
+ Object.entries(quantities).map(([k, v]) => [parseInt(k, 10), v])
643
+ ) : {};
644
+
645
+ const orderPayload = {
646
+ purchaserIP: ip,
647
+ eventID: eventID ? parseInt(eventID, 10) : undefined,
648
+ quantities: numericQuantities,
649
+ ...restCartData,
650
+ };
651
+
652
+ const res = await fetch(`https://get-micdrop.com/api/v2/public/orders/create`, {
653
+ method: "POST",
654
+ headers: {
655
+ "Content-Type": "application/json",
656
+ },
657
+ body: JSON.stringify(orderPayload),
658
+ });
659
+
660
+ const data = await res.json();
661
+
662
+ // Check for API errors
663
+ if (!res.ok || data.error) {
664
+ const errorMessage = data.error || data.message || 'Order creation failed';
665
+ // Check for inventory-related errors
666
+ const isInventoryError = errorMessage.toLowerCase().includes('inventory') ||
667
+ errorMessage.toLowerCase().includes('sold out') ||
668
+ errorMessage.toLowerCase().includes('not available') ||
669
+ errorMessage.toLowerCase().includes('insufficient') ||
670
+ res.status === 409; // Conflict status often used for inventory issues
671
+
672
+ return {
673
+ success: false,
674
+ error: errorMessage,
675
+ errorType: isInventoryError ? 'inventory' : 'server'
676
+ };
677
+ }
678
+
679
+ setCookie("checkout-cartid", data.uuid);
680
+ return { success: true, uuid: data.uuid };
681
+ } catch (err) {
682
+ console.error("Order initiation failed:", err);
683
+ return {
684
+ success: false,
685
+ error: err.message || 'Network error',
686
+ errorType: 'network'
687
+ };
688
+ }
689
+ }
690
+
691
+ export async function getOrder(orderId) {
692
+ try {
693
+ const response = await fetch(
694
+ `https://get-micdrop.com/api/v2/public/orders/${orderId}`,
695
+ {
696
+ method: "GET",
697
+ headers: {
698
+ "Content-Type": "application/json",
699
+ },
700
+ }
701
+ );
702
+
703
+ if (!response.ok) {
704
+ const errorData = await response.json().catch(() => ({}));
705
+ const errorMessage =
706
+ errorData.error || `Failed to fetch order (${response.status})`;
707
+ console.error("getOrder error response:", errorData);
708
+ throw new Error(errorMessage);
709
+ }
710
+
711
+ const data = await response.json();
712
+ return data;
713
+ } catch (err) {
714
+ console.error("getOrder error:", err);
715
+ throw err;
716
+ }
717
+ }
718
+
719
+ export async function createPaymentIntent(cartId, quantities) {
720
+ let ip = '';
721
+ try {
722
+ const ipRes = await fetch('https://api.ipify.org?format=json', {
723
+ signal: AbortSignal.timeout(5000) // 5 second timeout
724
+ });
725
+ if (ipRes.ok) {
726
+ const ipData = await ipRes.json();
727
+ ip = ipData.ip || '';
728
+ }
729
+ } catch (ipErr) {
730
+ console.warn('Failed to fetch IP address, continuing without it:', ipErr);
731
+ // Continue without IP if the service is unavailable
732
+ }
733
+
734
+ const token = getCookie('operator_token');
735
+
736
+ // Create an AbortController for timeout
737
+ const controller = new AbortController();
738
+ const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
739
+
740
+ try {
741
+ const res = await fetch(
742
+ `https://get-micdrop.com/api/v2/public/orders/${cartId}/payment-intent`,
743
+ {
744
+ method: 'POST',
745
+ headers: {
746
+ 'Content-Type': 'application/json',
747
+ Authorization: token ? `Bearer ${token}` : '',
748
+ },
749
+ credentials: 'include',
750
+ signal: controller.signal,
751
+ body: JSON.stringify({
752
+ IP: ip,
753
+ productQuantities: quantities,
754
+ }),
755
+ }
756
+ );
757
+
758
+ clearTimeout(timeoutId);
759
+
760
+ if (!res.ok) {
761
+ const errorData = await res.json().catch(() => ({}));
762
+ console.error('Payment intent creation failed:', errorData);
763
+ throw new Error(errorData.error || `Failed to create payment intent: ${res.status} ${res.statusText}`);
764
+ }
765
+
766
+ const data = await res.json();
767
+ return data;
768
+ } catch (err) {
769
+ clearTimeout(timeoutId);
770
+
771
+ // Provide more specific error messages
772
+ if (err.name === 'AbortError') {
773
+ console.error('createPaymentIntent error: Request timeout');
774
+ throw new Error('Request timed out. Please check your connection and try again.');
775
+ } else if (err.message && err.message.includes('Failed to fetch')) {
776
+ console.error('createPaymentIntent error: Network/CORS issue', err);
777
+ throw new Error('Unable to connect to payment server. Please check your internet connection or try again later.');
778
+ } else {
779
+ console.error('createPaymentIntent error:', err);
780
+ // Re-throw the error so the caller can handle it
781
+ throw err;
782
+ }
783
+ }
784
+ }
785
+
786
+ export async function validatePaymentIntent(cartId, payload) {
787
+ try {
788
+ const token = getCookie('operator_token');
789
+
790
+ const res = await fetch(`https://get-micdrop.com/api/v2/public/orders/${cartId}/validate-payment`, {
791
+ method: 'POST',
792
+ headers: {
793
+ 'Content-Type': 'application/json',
794
+ Authorization: token ? `Bearer ${token}` : '',
795
+ },
796
+ credentials: 'include',
797
+ body: JSON.stringify(payload),
798
+ });
799
+
800
+ const data = await res.json();
801
+ return data;
802
+ } catch (err) {
803
+ console.error('validatePaymentIntent error:', err);
804
+ return null;
805
+ }
806
+ }