@getmicdrop/venue-calendar 3.2.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.
- package/package.json +2 -1
- package/src/lib/utils/api.js +790 -0
- package/src/lib/utils/api.test.js +1284 -0
- package/src/lib/utils/constants.js +8 -0
- package/src/lib/utils/constants.test.js +39 -0
- package/src/lib/utils/datetime.js +266 -0
- package/src/lib/utils/datetime.test.js +340 -0
- package/src/lib/utils/event-transform.js +464 -0
- package/src/lib/utils/event-transform.test.js +413 -0
- package/src/lib/utils/logger.js +105 -0
- package/src/lib/utils/timezone.js +109 -0
- package/src/lib/utils/timezone.test.js +222 -0
- package/src/lib/utils/utils.js +806 -0
- package/src/lib/utils/utils.test.js +959 -0
|
@@ -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
|
+
}
|