@nuitee/booking-widget 1.0.2 → 1.0.4
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/USAGE.md +3 -3
- package/dist/booking-widget-standalone.js +228 -45
- package/dist/booking-widget.css +142 -2
- package/dist/booking-widget.js +224 -44
- package/dist/core/booking-api.js +11 -0
- package/dist/core/color-utils.js +103 -0
- package/dist/core/stripe-config.js +3 -1
- package/dist/core/styles.css +142 -2
- package/dist/react/BookingWidget.jsx +92 -29
- package/dist/react/styles.css +142 -2
- package/dist/utils/config-service.js +103 -0
- package/dist/vue/BookingWidget.vue +82 -20
- package/dist/vue/styles.css +142 -2
- package/package.json +2 -2
package/dist/booking-widget.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
(function(){'use strict';window.__BOOKING_WIDGET_API_BASE_URL__='https://ai.thehotelplanet.com';window.__BOOKING_WIDGET_STRIPE_KEY__='
|
|
1
|
+
(function(){'use strict';window.__BOOKING_WIDGET_API_BASE_URL__='https://ai.thehotelplanet.com';window.__BOOKING_WIDGET_STRIPE_KEY__='pk_test_51T0Se5EvQaQIixshRA8gbsI40NubMGwHlSWZmQM2LAx3jpVO0ThzVifqdTiMeGOyBfLNe8V9G8POGu8SjkvWOStM00H0i1U3uz';window.__BOOKING_WIDGET_DEFAULT_COLORS__={"background":"#022c32","text":"#ffffff","primary":"#f59e0b","primaryText":"#ffffff","card":"#395b60"};})();
|
|
2
2
|
// ===== Icons (Lucide/shadcn) =====
|
|
3
3
|
const icons = {
|
|
4
4
|
calendar: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>`,
|
|
@@ -51,6 +51,54 @@ function escapeHTML(value) {
|
|
|
51
51
|
.replace(/'/g, ''');
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
function deriveWidgetStyles(c) {
|
|
55
|
+
if (!c || (!c.background && !c.text && !c.primary && !c.primaryText)) return {};
|
|
56
|
+
var expandHex = function (hex) {
|
|
57
|
+
if (!hex || typeof hex !== 'string') return null;
|
|
58
|
+
var h = hex.replace(/^#/, '').trim();
|
|
59
|
+
if (h.length === 3) return '#' + h.split('').map(function (x) { return x + x; }).join('');
|
|
60
|
+
return h.length === 6 ? '#' + h : null;
|
|
61
|
+
};
|
|
62
|
+
var hexToRgb = function (hex) {
|
|
63
|
+
var x = expandHex(hex);
|
|
64
|
+
if (!x) return null;
|
|
65
|
+
return [parseInt(x.slice(1, 3), 16), parseInt(x.slice(3, 5), 16), parseInt(x.slice(5, 7), 16)];
|
|
66
|
+
};
|
|
67
|
+
var hexToHsl = function (hex) {
|
|
68
|
+
var x = expandHex(hex);
|
|
69
|
+
if (!x) return null;
|
|
70
|
+
var r = parseInt(x.slice(1, 3), 16) / 255, g = parseInt(x.slice(3, 5), 16) / 255, b = parseInt(x.slice(5, 7), 16) / 255;
|
|
71
|
+
var max = Math.max(r, g, b), min = Math.min(r, g, b), h, s, l = (max + min) / 2;
|
|
72
|
+
if (max === min) h = s = 0;
|
|
73
|
+
else {
|
|
74
|
+
var d = max - min;
|
|
75
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
76
|
+
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
77
|
+
else if (max === g) h = ((b - r) / d + 2) / 6;
|
|
78
|
+
else h = ((r - g) / d + 4) / 6;
|
|
79
|
+
}
|
|
80
|
+
return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
|
|
81
|
+
};
|
|
82
|
+
var bg = c.background || '#1a1a1a', fg = c.text || '#e0e0e0', primary = c.primary || '#3b82f6', primaryFg = c.primaryText || '#ffffff';
|
|
83
|
+
var primaryRgb = hexToRgb(primary), bgHsl = hexToHsl(bg), fgHsl = hexToHsl(fg);
|
|
84
|
+
var styles = { '--primary': primary, '--primary-fg': primaryFg, '--bg': bg, '--fg': fg, '--card-fg': fg };
|
|
85
|
+
if (primaryRgb) styles['--primary-rgb'] = primaryRgb[0] + ', ' + primaryRgb[1] + ', ' + primaryRgb[2];
|
|
86
|
+
styles['--card'] = c.card || (bgHsl ? 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 2) + '%)' : bg);
|
|
87
|
+
if (bgHsl) {
|
|
88
|
+
styles['--secondary'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 10) + '%)';
|
|
89
|
+
styles['--border'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 14) + '%)';
|
|
90
|
+
styles['--input-bg'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 6) + '%)';
|
|
91
|
+
}
|
|
92
|
+
if (fgHsl) {
|
|
93
|
+
styles['--secondary-fg'] = 'hsl(' + fgHsl[0] + ', ' + fgHsl[1] + '%, ' + Math.max(20, fgHsl[2] - 10) + '%)';
|
|
94
|
+
styles['--muted'] = 'hsl(' + fgHsl[0] + ', ' + fgHsl[1] + '%, ' + Math.max(30, fgHsl[2] - 25) + '%)';
|
|
95
|
+
}
|
|
96
|
+
styles['--font-serif'] = "'Playfair Display', Georgia, serif";
|
|
97
|
+
styles['--font-sans'] = "'Inter', system-ui, sans-serif";
|
|
98
|
+
styles['--radius'] = '0.75rem';
|
|
99
|
+
return styles;
|
|
100
|
+
}
|
|
101
|
+
|
|
54
102
|
function getDefaultRooms() {
|
|
55
103
|
return [];
|
|
56
104
|
}
|
|
@@ -59,6 +107,9 @@ function getDefaultRates() {
|
|
|
59
107
|
return [];
|
|
60
108
|
}
|
|
61
109
|
|
|
110
|
+
// In-memory cache shared across all widget instances (keyed by propertyKey).
|
|
111
|
+
var __bwConfigCache = {};
|
|
112
|
+
|
|
62
113
|
// ===== Core Widget Class =====
|
|
63
114
|
class BookingWidget {
|
|
64
115
|
constructor(options = {}) {
|
|
@@ -108,14 +159,20 @@ class BookingWidget {
|
|
|
108
159
|
s3BaseUrl: options.s3BaseUrl || null,
|
|
109
160
|
/** Base URL for confirmation status polling (same as API_BASE_URL). Default https://ai.thehotelplanet.com */
|
|
110
161
|
confirmationBaseUrl: options.confirmationBaseUrl || 'https://ai.thehotelplanet.com',
|
|
111
|
-
// Color customization
|
|
112
|
-
colors: {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
162
|
+
// Color customization: CONFIG defaults from load-config, installer colors override
|
|
163
|
+
colors: (function () {
|
|
164
|
+
const dc = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_DEFAULT_COLORS__) ? window.__BOOKING_WIDGET_DEFAULT_COLORS__ : {};
|
|
165
|
+
const inst = options.colors && typeof options.colors === 'object' ? options.colors : {};
|
|
166
|
+
return {
|
|
167
|
+
background: inst.background ?? dc.background ?? null,
|
|
168
|
+
text: inst.text ?? dc.text ?? null,
|
|
169
|
+
primary: inst.primary ?? dc.primary ?? null,
|
|
170
|
+
primaryText: inst.primaryText ?? dc.primaryText ?? null,
|
|
171
|
+
// When card omitted, use installer's background for consistency
|
|
172
|
+
card: inst.card ?? inst.background ?? dc.card ?? dc.background ?? null,
|
|
173
|
+
...inst
|
|
174
|
+
};
|
|
175
|
+
})(),
|
|
119
176
|
...options
|
|
120
177
|
};
|
|
121
178
|
|
|
@@ -166,6 +223,16 @@ class BookingWidget {
|
|
|
166
223
|
this.stripeInstance = null;
|
|
167
224
|
this.elementsInstance = null;
|
|
168
225
|
|
|
226
|
+
// Store raw installer colors so they can be re-merged with API-fetched colors.
|
|
227
|
+
this._rawInstallerColors = options.colors && typeof options.colors === 'object'
|
|
228
|
+
? Object.assign({}, options.colors)
|
|
229
|
+
: {};
|
|
230
|
+
|
|
231
|
+
// Config fetch state: 'idle' | 'loading' | 'loaded' | 'error'
|
|
232
|
+
this._configState = 'idle';
|
|
233
|
+
this._configError = null;
|
|
234
|
+
this._configPromise = null;
|
|
235
|
+
|
|
169
236
|
this.calendarMonth = null;
|
|
170
237
|
this.calendarYear = null;
|
|
171
238
|
this.pickState = 0;
|
|
@@ -229,8 +296,13 @@ class BookingWidget {
|
|
|
229
296
|
// ===== Widget Open/Close =====
|
|
230
297
|
open() {
|
|
231
298
|
if (!this.container) this.init();
|
|
299
|
+
if (!this.overlay || !this.widget) return; // container element not found
|
|
232
300
|
this.overlay.classList.add('active');
|
|
233
301
|
this.widget.classList.add('active');
|
|
302
|
+
// Only fetch config when propertyKey is present; missing-key is rendered inside the modal.
|
|
303
|
+
if (this.options.propertyKey && String(this.options.propertyKey).trim() && this._configState === 'idle') {
|
|
304
|
+
this._fetchRuntimeConfig();
|
|
305
|
+
}
|
|
234
306
|
this.render();
|
|
235
307
|
if (this.options.onOpen) this.options.onOpen();
|
|
236
308
|
}
|
|
@@ -266,6 +338,15 @@ class BookingWidget {
|
|
|
266
338
|
}
|
|
267
339
|
|
|
268
340
|
goToStep(step) {
|
|
341
|
+
// Block room/rate navigation until runtime config (colors + styles) has loaded.
|
|
342
|
+
if ((step === 'rooms' || step === 'rates') && this._configState !== 'loaded') {
|
|
343
|
+
if (this._configState === 'loading' && this._configPromise) {
|
|
344
|
+
this._configPromise.then(() => {
|
|
345
|
+
if (this._configState === 'loaded') this.goToStep(step);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
269
350
|
const doRender = () => {
|
|
270
351
|
if (step !== 'summary' && step !== 'payment') {
|
|
271
352
|
this.checkoutShowPaymentForm = false;
|
|
@@ -326,20 +407,19 @@ class BookingWidget {
|
|
|
326
407
|
}
|
|
327
408
|
|
|
328
409
|
this.container = container;
|
|
329
|
-
|
|
330
|
-
//
|
|
410
|
+
|
|
411
|
+
// Always create the overlay and modal so the error is shown inside the dialog,
|
|
412
|
+
// consistent with the React and Vue behaviour.
|
|
331
413
|
this.overlay = document.createElement('div');
|
|
332
414
|
this.overlay.className = 'booking-widget-overlay';
|
|
333
415
|
this.overlay.addEventListener('click', () => this.close());
|
|
334
416
|
container.appendChild(this.overlay);
|
|
335
417
|
|
|
336
|
-
// Create widget modal
|
|
337
418
|
this.widget = document.createElement('div');
|
|
338
419
|
this.widget.className = 'booking-widget-modal';
|
|
339
420
|
this.widget.innerHTML = `
|
|
340
421
|
<button class="booking-widget-close">${iconHTML('x', '1.5em')}</button>
|
|
341
422
|
<div class="booking-widget-step-indicator"></div>
|
|
342
|
-
<div class="booking-widget-property-key-message" role="alert" style="display:none;margin:0 1.5em 1em;padding:0.75em 1em;background:var(--destructive,#ef4444);color:#fff;border-radius:8px;font-size:0.9em;"></div>
|
|
343
423
|
<div class="booking-widget-step-content"></div>
|
|
344
424
|
`;
|
|
345
425
|
this.widget.querySelector('.booking-widget-close').addEventListener('click', () => this.close());
|
|
@@ -349,35 +429,85 @@ class BookingWidget {
|
|
|
349
429
|
});
|
|
350
430
|
container.appendChild(this.widget);
|
|
351
431
|
|
|
352
|
-
// Apply custom colors
|
|
353
432
|
this.applyColors();
|
|
354
|
-
|
|
355
|
-
// Inject CSS if not already present
|
|
356
433
|
this.injectCSS();
|
|
357
434
|
}
|
|
358
435
|
|
|
359
436
|
applyColors() {
|
|
360
437
|
if (!this.widget) return;
|
|
361
|
-
|
|
362
438
|
const colors = this.options.colors;
|
|
363
439
|
if (!colors) return;
|
|
364
|
-
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
440
|
+
const styles = deriveWidgetStyles(colors);
|
|
441
|
+
const el = this.widget.style;
|
|
442
|
+
for (var k in styles) if (styles[k] != null && styles[k] !== '') el.setProperty(k, styles[k]);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Merge API-fetched colors with installer-provided raw colors and update
|
|
447
|
+
* this.options.colors so applyColors() uses the runtime values.
|
|
448
|
+
*/
|
|
449
|
+
_applyApiColors(apiColors) {
|
|
450
|
+
const inst = this._rawInstallerColors || {};
|
|
451
|
+
this.options.colors = {
|
|
452
|
+
background: inst.background != null ? inst.background : (apiColors.background || null),
|
|
453
|
+
text: inst.text != null ? inst.text : (apiColors.text || null),
|
|
454
|
+
primary: inst.primary != null ? inst.primary : (apiColors.primary || null),
|
|
455
|
+
primaryText: inst.primaryText != null ? inst.primaryText : (apiColors.primaryText || null),
|
|
456
|
+
card: inst.card != null ? inst.card : (inst.background != null ? inst.background : (apiColors.card || null)),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Fetch runtime styling config from /load-config.
|
|
462
|
+
* Caches result in __bwConfigCache (module-level, shared across instances).
|
|
463
|
+
* On success: merges colors, applies them, and re-renders.
|
|
464
|
+
* On error: sets _configState to 'error' and re-renders to show the error banner.
|
|
465
|
+
*/
|
|
466
|
+
_fetchRuntimeConfig() {
|
|
467
|
+
const self = this;
|
|
468
|
+
const propertyKey = this.options.propertyKey;
|
|
469
|
+
const key = String(propertyKey).trim();
|
|
470
|
+
const isSandbox = this.options.mode === 'sandbox';
|
|
471
|
+
// Cache key is mode-aware so sandbox and live configs are stored separately.
|
|
472
|
+
const cacheKey = isSandbox ? key + ':sandbox' : key;
|
|
473
|
+
|
|
474
|
+
if (__bwConfigCache[cacheKey]) {
|
|
475
|
+
this._applyApiColors(__bwConfigCache[cacheKey]);
|
|
476
|
+
this._configState = 'loaded';
|
|
477
|
+
this.applyColors();
|
|
478
|
+
return Promise.resolve(__bwConfigCache[cacheKey]);
|
|
380
479
|
}
|
|
480
|
+
|
|
481
|
+
this._configState = 'loading';
|
|
482
|
+
this.render();
|
|
483
|
+
|
|
484
|
+
const url = 'https://ai.thehotelplanet.com/load-config?apikey=' + encodeURIComponent(key) + (isSandbox ? '&mode=sandbox' : '');
|
|
485
|
+
this._configPromise = fetch(url)
|
|
486
|
+
.then(function (res) {
|
|
487
|
+
if (!res.ok) throw new Error('Failed to load widget configuration (HTTP ' + res.status + ').');
|
|
488
|
+
return res.json();
|
|
489
|
+
})
|
|
490
|
+
.then(function (data) {
|
|
491
|
+
const apiColors = {};
|
|
492
|
+
if (data.widgetBackground) apiColors.background = data.widgetBackground;
|
|
493
|
+
if (data.widgetTextColor) apiColors.text = data.widgetTextColor;
|
|
494
|
+
if (data.primaryColor) apiColors.primary = data.primaryColor;
|
|
495
|
+
if (data.buttonTextColor) apiColors.primaryText = data.buttonTextColor;
|
|
496
|
+
if (data.widgetCardColor) apiColors.card = data.widgetCardColor;
|
|
497
|
+
__bwConfigCache[cacheKey] = apiColors;
|
|
498
|
+
self._applyApiColors(apiColors);
|
|
499
|
+
self._configState = 'loaded';
|
|
500
|
+
self.applyColors();
|
|
501
|
+
self.render();
|
|
502
|
+
return apiColors;
|
|
503
|
+
})
|
|
504
|
+
.catch(function (err) {
|
|
505
|
+
self._configError = err.message || 'Failed to load widget configuration.';
|
|
506
|
+
self._configState = 'error';
|
|
507
|
+
self.render();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
return this._configPromise;
|
|
381
511
|
}
|
|
382
512
|
|
|
383
513
|
injectCSS() {
|
|
@@ -411,7 +541,7 @@ class BookingWidget {
|
|
|
411
541
|
if (!el) return;
|
|
412
542
|
const key = this.options.propertyKey;
|
|
413
543
|
const hasPropertyKey = key != null && key !== '' && (typeof key !== 'string' || key.trim() !== '');
|
|
414
|
-
if (!hasPropertyKey || this.state.step === 'confirmation') {
|
|
544
|
+
if (!hasPropertyKey || this._configState !== 'loaded' || this.state.step === 'confirmation') {
|
|
415
545
|
el.innerHTML = '';
|
|
416
546
|
return;
|
|
417
547
|
}
|
|
@@ -434,23 +564,73 @@ class BookingWidget {
|
|
|
434
564
|
renderPropertyKeyMessage() {
|
|
435
565
|
const el = this.widget.querySelector('.booking-widget-property-key-message');
|
|
436
566
|
if (!el) return;
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
567
|
+
// Config errors are now rendered inside renderStepContent via the full error card.
|
|
568
|
+
el.style.display = 'none';
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
_retryConfigFetch() {
|
|
572
|
+
this._configState = 'idle';
|
|
573
|
+
this._configError = null;
|
|
574
|
+
this._configPromise = null;
|
|
575
|
+
this._fetchRuntimeConfig();
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
_configErrorHTML(type, title, body, hint) {
|
|
579
|
+
const lockIcon = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>`;
|
|
580
|
+
const warnIcon = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>`;
|
|
581
|
+
const retryIcon = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 4v6h-6"></path><path d="M1 20v-6h6"></path><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`;
|
|
582
|
+
const icon = type === 'missing' ? lockIcon : warnIcon;
|
|
583
|
+
const badgeLabel = type === 'missing'
|
|
584
|
+
? `<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg> Missing Configuration`
|
|
585
|
+
: 'Configuration Error';
|
|
586
|
+
const retryBtn = type === 'fetch'
|
|
587
|
+
? `<button class="booking-widget-config-error__retry" onclick="window.bookingWidgetInstance && window.bookingWidgetInstance._retryConfigFetch()">${retryIcon} Try Again</button>`
|
|
588
|
+
: '';
|
|
589
|
+
return `<div class="booking-widget-config-error" role="alert">
|
|
590
|
+
<div class="booking-widget-config-error__icon-wrap">${icon}</div>
|
|
591
|
+
<span class="booking-widget-config-error__badge">${badgeLabel}</span>
|
|
592
|
+
<h3 class="booking-widget-config-error__title">${escapeHTML(title)}</h3>
|
|
593
|
+
<p class="booking-widget-config-error__desc">${body}</p>
|
|
594
|
+
<div class="booking-widget-config-error__divider"></div>
|
|
595
|
+
<p class="booking-widget-config-error__hint">${escapeHTML(hint)}</p>
|
|
596
|
+
${retryBtn}
|
|
597
|
+
</div>`;
|
|
445
598
|
}
|
|
446
599
|
|
|
447
600
|
// ===== Step Renderers =====
|
|
448
601
|
renderStepContent() {
|
|
449
602
|
const el = this.widget.querySelector('.booking-widget-step-content');
|
|
450
603
|
if (!el) return;
|
|
604
|
+
|
|
605
|
+
// Missing propertyKey — show error card inside the modal, same as React/Vue.
|
|
451
606
|
const key = this.options.propertyKey;
|
|
452
|
-
|
|
453
|
-
|
|
607
|
+
if (!key || !String(key).trim()) {
|
|
608
|
+
el.innerHTML = this._configErrorHTML(
|
|
609
|
+
'missing',
|
|
610
|
+
'Widget Not Configured',
|
|
611
|
+
'A <code>propertyKey</code> option is required to initialize this booking widget. Please provide it to load rooms and availability.',
|
|
612
|
+
'Contact the site administrator to configure this widget.'
|
|
613
|
+
);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (this._configState === 'loading') {
|
|
618
|
+
el.innerHTML = `<div class="booking-widget-config-loading">
|
|
619
|
+
<div class="booking-widget-config-loading__spinner"></div>
|
|
620
|
+
<span class="booking-widget-config-loading__text">Loading configuration\u2026</span>
|
|
621
|
+
</div>`;
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (this._configState === 'error') {
|
|
625
|
+
el.innerHTML = this._configErrorHTML(
|
|
626
|
+
'fetch',
|
|
627
|
+
'Could Not Load Config',
|
|
628
|
+
escapeHTML(this._configError || 'An unexpected error occurred.'),
|
|
629
|
+
'Please try again or contact support.'
|
|
630
|
+
);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (this._configState !== 'loaded') {
|
|
454
634
|
el.innerHTML = '';
|
|
455
635
|
return;
|
|
456
636
|
}
|
package/dist/core/booking-api.js
CHANGED
|
@@ -618,6 +618,16 @@ function buildCheckoutPayload(state, options = {}) {
|
|
|
618
618
|
* @param {Object} [options] - { propertyKey: string, sandbox?: boolean }
|
|
619
619
|
* @returns {{ rate_identifier: string, key: string, metadata: Object, sandbox?: boolean }}
|
|
620
620
|
*/
|
|
621
|
+
function generateUUID() {
|
|
622
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
623
|
+
return crypto.randomUUID();
|
|
624
|
+
}
|
|
625
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
626
|
+
const r = Math.random() * 16 | 0;
|
|
627
|
+
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
621
631
|
function buildPaymentIntentPayload(state, options = {}) {
|
|
622
632
|
const rawKey = options.propertyKey != null && options.propertyKey !== '' ? String(options.propertyKey).trim() : '';
|
|
623
633
|
const propertyKey = rawKey && rawKey.startsWith('book_engine_') ? rawKey : '';
|
|
@@ -657,6 +667,7 @@ function buildPaymentIntentPayload(state, options = {}) {
|
|
|
657
667
|
email: g.email ?? '',
|
|
658
668
|
occupancies: occupanciesForPayload,
|
|
659
669
|
source_transaction: 'booking engine',
|
|
670
|
+
booking_code: generateUUID(),
|
|
660
671
|
},
|
|
661
672
|
};
|
|
662
673
|
if (sandbox) out.sandbox = true;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color utilities for widget theming. Installers pass 4 base colors (background, text, primary, primaryText);
|
|
3
|
+
* we derive --card, --secondary, --muted, --border, --input-bg for a coherent theme.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function expandHex(hex) {
|
|
7
|
+
if (!hex || typeof hex !== 'string') return null;
|
|
8
|
+
const h = hex.replace(/^#/, '').trim();
|
|
9
|
+
if (h.length === 3) return '#' + h.split('').map((c) => c + c).join('');
|
|
10
|
+
return h.length === 6 ? '#' + h : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hexToRgb(hex) {
|
|
14
|
+
const expanded = expandHex(hex);
|
|
15
|
+
if (!expanded) return null;
|
|
16
|
+
return [
|
|
17
|
+
parseInt(expanded.slice(1, 3), 16),
|
|
18
|
+
parseInt(expanded.slice(3, 5), 16),
|
|
19
|
+
parseInt(expanded.slice(5, 7), 16),
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hexToHsl(hex) {
|
|
24
|
+
const expanded = expandHex(hex);
|
|
25
|
+
if (!expanded) return null;
|
|
26
|
+
let r = parseInt(expanded.slice(1, 3), 16) / 255;
|
|
27
|
+
let g = parseInt(expanded.slice(3, 5), 16) / 255;
|
|
28
|
+
let b = parseInt(expanded.slice(5, 7), 16) / 255;
|
|
29
|
+
const max = Math.max(r, g, b);
|
|
30
|
+
const min = Math.min(r, g, b);
|
|
31
|
+
let h, s;
|
|
32
|
+
const l = (max + min) / 2;
|
|
33
|
+
if (max === min) {
|
|
34
|
+
h = s = 0;
|
|
35
|
+
} else {
|
|
36
|
+
const d = max - min;
|
|
37
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
38
|
+
switch (max) {
|
|
39
|
+
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
|
40
|
+
case g: h = ((b - r) / d + 2) / 6; break;
|
|
41
|
+
case b: h = ((r - g) / d + 4) / 6; break;
|
|
42
|
+
default: h = 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Derive full widget CSS variables from 4 base colors.
|
|
50
|
+
* @param {Object} c - { background, text, primary, primaryText, card? }
|
|
51
|
+
* @returns {Object} CSS variable names to values, e.g. { '--bg': '#1a1a1a', ... }
|
|
52
|
+
*/
|
|
53
|
+
export function deriveWidgetStyles(c) {
|
|
54
|
+
if (!c || (!c.background && !c.text && !c.primary && !c.primaryText)) return {};
|
|
55
|
+
const bg = c.background || '#1a1a1a';
|
|
56
|
+
const fg = c.text || '#e0e0e0';
|
|
57
|
+
const primary = c.primary || '#3b82f6';
|
|
58
|
+
const primaryFg = c.primaryText || '#ffffff';
|
|
59
|
+
const cardExplicit = c.card;
|
|
60
|
+
|
|
61
|
+
const primaryRgb = hexToRgb(primary);
|
|
62
|
+
const bgHsl = hexToHsl(bg);
|
|
63
|
+
const fgHsl = hexToHsl(fg);
|
|
64
|
+
|
|
65
|
+
const styles = {
|
|
66
|
+
'--primary': primary,
|
|
67
|
+
'--primary-fg': primaryFg,
|
|
68
|
+
'--bg': bg,
|
|
69
|
+
'--fg': fg,
|
|
70
|
+
'--card-fg': fg,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (primaryRgb) styles['--primary-rgb'] = `${primaryRgb[0]}, ${primaryRgb[1]}, ${primaryRgb[2]}`;
|
|
74
|
+
|
|
75
|
+
if (cardExplicit) {
|
|
76
|
+
styles['--card'] = cardExplicit + "40";
|
|
77
|
+
styles['--card-solid'] = cardExplicit;
|
|
78
|
+
} else if (bgHsl) {
|
|
79
|
+
const cardVal = `hsl(${bgHsl[0]}, ${bgHsl[1]}%, ${Math.min(95, bgHsl[2] + 2)}%)`;
|
|
80
|
+
styles['--card'] = cardVal;
|
|
81
|
+
styles['--card-solid'] = cardVal;
|
|
82
|
+
} else {
|
|
83
|
+
styles['--card'] = bg;
|
|
84
|
+
styles['--card-solid'] = bg;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (bgHsl) {
|
|
88
|
+
styles['--secondary'] = `hsl(${bgHsl[0]}, ${bgHsl[1]}%, ${Math.min(95, bgHsl[2] + 10)}%)`;
|
|
89
|
+
styles['--border'] = `hsl(${bgHsl[0]}, ${bgHsl[1]}%, ${Math.min(95, bgHsl[2] + 14)}%)`;
|
|
90
|
+
styles['--input-bg'] = `hsl(${bgHsl[0]}, ${bgHsl[1]}%, ${Math.min(95, bgHsl[2] + 6)}%)`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (fgHsl) {
|
|
94
|
+
styles['--secondary-fg'] = `hsl(${fgHsl[0]}, ${fgHsl[1]}%, ${Math.max(20, fgHsl[2] - 10)}%)`;
|
|
95
|
+
styles['--muted'] = `hsl(${fgHsl[0]}, ${fgHsl[1]}%, ${Math.max(30, fgHsl[2] - 25)}%)`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
styles['--font-serif'] = "'Playfair Display', Georgia, serif";
|
|
99
|
+
styles['--font-sans'] = "'Inter', system-ui, sans-serif";
|
|
100
|
+
styles['--radius'] = '0.75rem';
|
|
101
|
+
|
|
102
|
+
return styles;
|
|
103
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Config from CONFIG_URL (default https://ai.thehotelplanet.com/load-config). No .env used.
|
|
3
3
|
* Property key is NOT from load-config; installers must set it (VITE_PROPERTY_KEY in app .env or propertyKey option).
|
|
4
|
+
* DEFAULT_COLORS from load-config CONFIG; installers can override via colors prop.
|
|
4
5
|
*/
|
|
5
|
-
export const STRIPE_PUBLISHABLE_KEY = '
|
|
6
|
+
export const STRIPE_PUBLISHABLE_KEY = 'pk_test_51T0Se5EvQaQIixshRA8gbsI40NubMGwHlSWZmQM2LAx3jpVO0ThzVifqdTiMeGOyBfLNe8V9G8POGu8SjkvWOStM00H0i1U3uz';
|
|
6
7
|
export const API_BASE_URL = 'https://ai.thehotelplanet.com';
|
|
7
8
|
export const VITE_CONFIG_BASE_URL = 'https://ai.thehotelplanet.com/api';
|
|
8
9
|
export const VITE_AWS_S3_PATH = 'https://nuitee-s3-temp.s3.us-west-1.amazonaws.com';
|
|
10
|
+
export const DEFAULT_COLORS = {"background":"#022c32","text":"#ffffff","primary":"#f59e0b","primaryText":"#ffffff","card":"#395b60"};
|
package/dist/core/styles.css
CHANGED
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
--bg: hsl(30, 10%, 8%);
|
|
82
82
|
--fg: hsl(40, 20%, 90%);
|
|
83
83
|
--card: hsl(30, 8%, 12%);
|
|
84
|
+
--card-solid: hsl(30, 8%, 12%);
|
|
84
85
|
--card-fg: hsl(40, 20%, 90%);
|
|
85
86
|
--primary: hsl(38, 60%, 55%);
|
|
86
87
|
--primary-fg: hsl(30, 10%, 8%);
|
|
@@ -106,6 +107,145 @@
|
|
|
106
107
|
font-weight: 500;
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
/* ===== Config Error State ===== */
|
|
111
|
+
.booking-widget-config-error {
|
|
112
|
+
display: flex;
|
|
113
|
+
flex-direction: column;
|
|
114
|
+
align-items: center;
|
|
115
|
+
justify-content: center;
|
|
116
|
+
gap: 1.1em;
|
|
117
|
+
padding: 4em 2em;
|
|
118
|
+
text-align: center;
|
|
119
|
+
min-height: 300px;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.booking-widget-config-error__icon-wrap {
|
|
123
|
+
width: 5em;
|
|
124
|
+
height: 5em;
|
|
125
|
+
border-radius: 50%;
|
|
126
|
+
background: radial-gradient(circle, rgba(239,68,68,0.12) 0%, rgba(239,68,68,0.04) 100%);
|
|
127
|
+
border: 1.5px solid rgba(239, 68, 68, 0.22);
|
|
128
|
+
box-shadow: 0 0 0 6px rgba(239, 68, 68, 0.06);
|
|
129
|
+
display: flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
justify-content: center;
|
|
132
|
+
color: #f87171;
|
|
133
|
+
flex-shrink: 0;
|
|
134
|
+
margin-bottom: 0.25em;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.booking-widget-config-error__badge {
|
|
138
|
+
display: inline-flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
gap: 0.4em;
|
|
141
|
+
font-size: 0.7em;
|
|
142
|
+
font-weight: 600;
|
|
143
|
+
letter-spacing: 0.12em;
|
|
144
|
+
text-transform: uppercase;
|
|
145
|
+
color: #f87171;
|
|
146
|
+
background: rgba(239, 68, 68, 0.1);
|
|
147
|
+
border: 1px solid rgba(239, 68, 68, 0.18);
|
|
148
|
+
border-radius: 99em;
|
|
149
|
+
padding: 0.3em 0.85em;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.booking-widget-config-error__title {
|
|
153
|
+
font-family: var(--font-serif, 'Playfair Display', Georgia, serif);
|
|
154
|
+
font-size: 1.35em;
|
|
155
|
+
font-weight: 600;
|
|
156
|
+
color: var(--fg, #e8e0d5);
|
|
157
|
+
margin: 0;
|
|
158
|
+
letter-spacing: -0.01em;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.booking-widget-config-error__desc {
|
|
162
|
+
font-size: 0.875em;
|
|
163
|
+
color: var(--secondary-fg, #a09080);
|
|
164
|
+
max-width: 25em;
|
|
165
|
+
line-height: 1.7;
|
|
166
|
+
margin: 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.booking-widget-config-error__desc code {
|
|
170
|
+
font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', Menlo, monospace;
|
|
171
|
+
font-size: 0.88em;
|
|
172
|
+
background: rgba(255, 255, 255, 0.06);
|
|
173
|
+
color: var(--primary, #f59e0b);
|
|
174
|
+
padding: 0.12em 0.45em;
|
|
175
|
+
border-radius: 0.3em;
|
|
176
|
+
border: 1px solid rgba(255, 255, 255, 0.09);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.booking-widget-config-error__divider {
|
|
180
|
+
width: 2.5em;
|
|
181
|
+
height: 1.5px;
|
|
182
|
+
background: var(--border, rgba(255,255,255,0.1));
|
|
183
|
+
border-radius: 1px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.booking-widget-config-error__hint {
|
|
187
|
+
font-size: 0.78em;
|
|
188
|
+
color: var(--muted, #6b5f50);
|
|
189
|
+
max-width: 21em;
|
|
190
|
+
line-height: 1.6;
|
|
191
|
+
margin: 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.booking-widget-config-error__retry {
|
|
195
|
+
display: inline-flex;
|
|
196
|
+
align-items: center;
|
|
197
|
+
gap: 0.45em;
|
|
198
|
+
padding: 0.55em 1.4em;
|
|
199
|
+
background: transparent;
|
|
200
|
+
color: var(--secondary-fg, #a09080);
|
|
201
|
+
border: 1.5px solid var(--border, rgba(255,255,255,0.13));
|
|
202
|
+
border-radius: 99em;
|
|
203
|
+
font-size: 0.8em;
|
|
204
|
+
font-family: var(--font-sans, system-ui, sans-serif);
|
|
205
|
+
font-weight: 500;
|
|
206
|
+
cursor: pointer;
|
|
207
|
+
letter-spacing: 0.02em;
|
|
208
|
+
transition: border-color 0.2s, color 0.2s, background 0.2s;
|
|
209
|
+
margin-top: 0.25em;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.booking-widget-config-error__retry:hover {
|
|
213
|
+
border-color: var(--primary, #f59e0b);
|
|
214
|
+
color: var(--primary, #f59e0b);
|
|
215
|
+
background: rgba(245, 158, 11, 0.06);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* ===== Config Loading State ===== */
|
|
219
|
+
.booking-widget-config-loading {
|
|
220
|
+
display: flex;
|
|
221
|
+
flex-direction: column;
|
|
222
|
+
align-items: center;
|
|
223
|
+
justify-content: center;
|
|
224
|
+
gap: 1.25em;
|
|
225
|
+
padding: 4em 2em;
|
|
226
|
+
text-align: center;
|
|
227
|
+
min-height: 300px;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.booking-widget-config-loading__spinner {
|
|
231
|
+
width: 2.75em;
|
|
232
|
+
height: 2.75em;
|
|
233
|
+
border: 2px solid var(--border, rgba(255,255,255,0.1));
|
|
234
|
+
border-top-color: var(--primary, hsl(38,60%,55%));
|
|
235
|
+
border-radius: 50%;
|
|
236
|
+
animation: bw-spin 0.75s linear infinite;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@keyframes bw-spin {
|
|
240
|
+
to { transform: rotate(360deg); }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.booking-widget-config-loading__text {
|
|
244
|
+
font-size: 0.875em;
|
|
245
|
+
color: var(--muted, #888);
|
|
246
|
+
letter-spacing: 0.01em;
|
|
247
|
+
}
|
|
248
|
+
|
|
109
249
|
/* ===== Step Indicator ===== */
|
|
110
250
|
.booking-widget-step-indicator {
|
|
111
251
|
display: flex;
|
|
@@ -448,7 +588,7 @@
|
|
|
448
588
|
}
|
|
449
589
|
|
|
450
590
|
.booking-widget-modal .date-trigger:hover {
|
|
451
|
-
border-color:
|
|
591
|
+
border-color: var(--primary);
|
|
452
592
|
}
|
|
453
593
|
|
|
454
594
|
.booking-widget-modal .date-trigger .placeholder {
|
|
@@ -472,7 +612,7 @@
|
|
|
472
612
|
max-width: calc(100vw - 2em);
|
|
473
613
|
box-sizing: border-box;
|
|
474
614
|
z-index: 10;
|
|
475
|
-
background: var(--card);
|
|
615
|
+
background: var(--card-solid);
|
|
476
616
|
border: 1px solid var(--border);
|
|
477
617
|
border-radius: var(--radius);
|
|
478
618
|
padding: 1em;
|