@nuitee/booking-widget 1.0.1 → 1.0.3
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 +292 -69
- package/dist/booking-widget.css +141 -2
- package/dist/booking-widget.js +246 -38
- package/dist/core/booking-api.js +30 -8
- package/dist/core/color-utils.js +103 -0
- package/dist/core/stripe-config.js +2 -0
- package/dist/core/styles.css +141 -2
- package/dist/react/BookingWidget.jsx +111 -36
- package/dist/react/styles.css +141 -2
- package/dist/utils/config-service.js +99 -0
- package/dist/vue/BookingWidget.vue +96 -24
- package/dist/vue/styles.css +141 -2
- package/package.json +2 -2
package/dist/booking-widget.css
CHANGED
|
@@ -106,6 +106,145 @@
|
|
|
106
106
|
font-weight: 500;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/* ===== Config Error State ===== */
|
|
110
|
+
.booking-widget-config-error {
|
|
111
|
+
display: flex;
|
|
112
|
+
flex-direction: column;
|
|
113
|
+
align-items: center;
|
|
114
|
+
justify-content: center;
|
|
115
|
+
gap: 1.1em;
|
|
116
|
+
padding: 4em 2em;
|
|
117
|
+
text-align: center;
|
|
118
|
+
min-height: 300px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.booking-widget-config-error__icon-wrap {
|
|
122
|
+
width: 5em;
|
|
123
|
+
height: 5em;
|
|
124
|
+
border-radius: 50%;
|
|
125
|
+
background: radial-gradient(circle, rgba(239,68,68,0.12) 0%, rgba(239,68,68,0.04) 100%);
|
|
126
|
+
border: 1.5px solid rgba(239, 68, 68, 0.22);
|
|
127
|
+
box-shadow: 0 0 0 6px rgba(239, 68, 68, 0.06);
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
justify-content: center;
|
|
131
|
+
color: #f87171;
|
|
132
|
+
flex-shrink: 0;
|
|
133
|
+
margin-bottom: 0.25em;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.booking-widget-config-error__badge {
|
|
137
|
+
display: inline-flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
gap: 0.4em;
|
|
140
|
+
font-size: 0.7em;
|
|
141
|
+
font-weight: 600;
|
|
142
|
+
letter-spacing: 0.12em;
|
|
143
|
+
text-transform: uppercase;
|
|
144
|
+
color: #f87171;
|
|
145
|
+
background: rgba(239, 68, 68, 0.1);
|
|
146
|
+
border: 1px solid rgba(239, 68, 68, 0.18);
|
|
147
|
+
border-radius: 99em;
|
|
148
|
+
padding: 0.3em 0.85em;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.booking-widget-config-error__title {
|
|
152
|
+
font-family: var(--font-serif, 'Playfair Display', Georgia, serif);
|
|
153
|
+
font-size: 1.35em;
|
|
154
|
+
font-weight: 600;
|
|
155
|
+
color: var(--fg, #e8e0d5);
|
|
156
|
+
margin: 0;
|
|
157
|
+
letter-spacing: -0.01em;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.booking-widget-config-error__desc {
|
|
161
|
+
font-size: 0.875em;
|
|
162
|
+
color: var(--secondary-fg, #a09080);
|
|
163
|
+
max-width: 25em;
|
|
164
|
+
line-height: 1.7;
|
|
165
|
+
margin: 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.booking-widget-config-error__desc code {
|
|
169
|
+
font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', Menlo, monospace;
|
|
170
|
+
font-size: 0.88em;
|
|
171
|
+
background: rgba(255, 255, 255, 0.06);
|
|
172
|
+
color: var(--primary, #f59e0b);
|
|
173
|
+
padding: 0.12em 0.45em;
|
|
174
|
+
border-radius: 0.3em;
|
|
175
|
+
border: 1px solid rgba(255, 255, 255, 0.09);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.booking-widget-config-error__divider {
|
|
179
|
+
width: 2.5em;
|
|
180
|
+
height: 1.5px;
|
|
181
|
+
background: var(--border, rgba(255,255,255,0.1));
|
|
182
|
+
border-radius: 1px;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.booking-widget-config-error__hint {
|
|
186
|
+
font-size: 0.78em;
|
|
187
|
+
color: var(--muted, #6b5f50);
|
|
188
|
+
max-width: 21em;
|
|
189
|
+
line-height: 1.6;
|
|
190
|
+
margin: 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.booking-widget-config-error__retry {
|
|
194
|
+
display: inline-flex;
|
|
195
|
+
align-items: center;
|
|
196
|
+
gap: 0.45em;
|
|
197
|
+
padding: 0.55em 1.4em;
|
|
198
|
+
background: transparent;
|
|
199
|
+
color: var(--secondary-fg, #a09080);
|
|
200
|
+
border: 1.5px solid var(--border, rgba(255,255,255,0.13));
|
|
201
|
+
border-radius: 99em;
|
|
202
|
+
font-size: 0.8em;
|
|
203
|
+
font-family: var(--font-sans, system-ui, sans-serif);
|
|
204
|
+
font-weight: 500;
|
|
205
|
+
cursor: pointer;
|
|
206
|
+
letter-spacing: 0.02em;
|
|
207
|
+
transition: border-color 0.2s, color 0.2s, background 0.2s;
|
|
208
|
+
margin-top: 0.25em;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.booking-widget-config-error__retry:hover {
|
|
212
|
+
border-color: var(--primary, #f59e0b);
|
|
213
|
+
color: var(--primary, #f59e0b);
|
|
214
|
+
background: rgba(245, 158, 11, 0.06);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* ===== Config Loading State ===== */
|
|
218
|
+
.booking-widget-config-loading {
|
|
219
|
+
display: flex;
|
|
220
|
+
flex-direction: column;
|
|
221
|
+
align-items: center;
|
|
222
|
+
justify-content: center;
|
|
223
|
+
gap: 1.25em;
|
|
224
|
+
padding: 4em 2em;
|
|
225
|
+
text-align: center;
|
|
226
|
+
min-height: 300px;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.booking-widget-config-loading__spinner {
|
|
230
|
+
width: 2.75em;
|
|
231
|
+
height: 2.75em;
|
|
232
|
+
border: 2px solid var(--border, rgba(255,255,255,0.1));
|
|
233
|
+
border-top-color: var(--primary, hsl(38,60%,55%));
|
|
234
|
+
border-radius: 50%;
|
|
235
|
+
animation: bw-spin 0.75s linear infinite;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
@keyframes bw-spin {
|
|
239
|
+
to { transform: rotate(360deg); }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.booking-widget-config-loading__text {
|
|
243
|
+
font-size: 0.875em;
|
|
244
|
+
color: var(--muted, #888);
|
|
245
|
+
letter-spacing: 0.01em;
|
|
246
|
+
}
|
|
247
|
+
|
|
109
248
|
/* ===== Step Indicator ===== */
|
|
110
249
|
.booking-widget-step-indicator {
|
|
111
250
|
display: flex;
|
|
@@ -448,7 +587,7 @@
|
|
|
448
587
|
}
|
|
449
588
|
|
|
450
589
|
.booking-widget-modal .date-trigger:hover {
|
|
451
|
-
border-color:
|
|
590
|
+
border-color: var(--primary);
|
|
452
591
|
}
|
|
453
592
|
|
|
454
593
|
.booking-widget-modal .date-trigger .placeholder {
|
|
@@ -472,7 +611,7 @@
|
|
|
472
611
|
max-width: calc(100vw - 2em);
|
|
473
612
|
box-sizing: border-box;
|
|
474
613
|
z-index: 10;
|
|
475
|
-
background: var(--card);
|
|
614
|
+
background: var(--card-solid, var(--card));
|
|
476
615
|
border: 1px solid var(--border);
|
|
477
616
|
border-radius: var(--radius);
|
|
478
617
|
padding: 1em;
|
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__='pk_test_51T0Se5EvQaQIixshRA8gbsI40NubMGwHlSWZmQM2LAx3jpVO0ThzVifqdTiMeGOyBfLNe8V9G8POGu8SjkvWOStM00H0i1U3uz';})();
|
|
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,14 +107,19 @@ 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 = {}) {
|
|
65
116
|
const apiBase = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_API_BASE_URL__) ? window.__BOOKING_WIDGET_API_BASE_URL__ : '';
|
|
66
117
|
const builtInStripeKey = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_STRIPE_KEY__) ? window.__BOOKING_WIDGET_STRIPE_KEY__ : '';
|
|
118
|
+
const builtInMode = options.mode;
|
|
67
119
|
const builtInCreatePaymentIntent = (apiBase && typeof window !== 'undefined')
|
|
68
120
|
? function (payload) {
|
|
69
|
-
|
|
121
|
+
const url = apiBase.replace(/\/$/, '') + '/proxy/create-payment-intent' + (builtInMode === 'sandbox' ? '?sandbox=true' : '');
|
|
122
|
+
return fetch(url, {
|
|
70
123
|
method: 'POST',
|
|
71
124
|
headers: { 'Content-Type': 'application/json' },
|
|
72
125
|
body: JSON.stringify(payload),
|
|
@@ -99,19 +152,27 @@ class BookingWidget {
|
|
|
99
152
|
apiSecret: options.apiSecret || null,
|
|
100
153
|
propertyId: options.propertyId != null && options.propertyId !== '' ? options.propertyId : null,
|
|
101
154
|
propertyKey: options.propertyKey || null,
|
|
155
|
+
/** 'sandbox' to pass sandbox=true on all proxy/API calls; omit or 'live' to not pass it. */
|
|
156
|
+
mode: options.mode || null,
|
|
102
157
|
availabilityBaseUrl: options.availabilityBaseUrl || null,
|
|
103
158
|
propertyBaseUrl: options.propertyBaseUrl || null,
|
|
104
159
|
s3BaseUrl: options.s3BaseUrl || null,
|
|
105
160
|
/** Base URL for confirmation status polling (same as API_BASE_URL). Default https://ai.thehotelplanet.com */
|
|
106
161
|
confirmationBaseUrl: options.confirmationBaseUrl || 'https://ai.thehotelplanet.com',
|
|
107
|
-
// Color customization
|
|
108
|
-
colors: {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
})(),
|
|
115
176
|
...options
|
|
116
177
|
};
|
|
117
178
|
|
|
@@ -125,6 +186,7 @@ class BookingWidget {
|
|
|
125
186
|
s3BaseUrl: opts.s3BaseUrl || undefined,
|
|
126
187
|
propertyId: opts.propertyId != null && opts.propertyId !== '' ? String(opts.propertyId) : undefined,
|
|
127
188
|
propertyKey: opts.propertyKey || undefined,
|
|
189
|
+
mode: opts.mode === 'sandbox' ? 'sandbox' : undefined,
|
|
128
190
|
headers: opts.apiSecret ? { 'X-API-Key': opts.apiSecret } : undefined,
|
|
129
191
|
})
|
|
130
192
|
: null);
|
|
@@ -161,6 +223,16 @@ class BookingWidget {
|
|
|
161
223
|
this.stripeInstance = null;
|
|
162
224
|
this.elementsInstance = null;
|
|
163
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
|
+
|
|
164
236
|
this.calendarMonth = null;
|
|
165
237
|
this.calendarYear = null;
|
|
166
238
|
this.pickState = 0;
|
|
@@ -224,8 +296,13 @@ class BookingWidget {
|
|
|
224
296
|
// ===== Widget Open/Close =====
|
|
225
297
|
open() {
|
|
226
298
|
if (!this.container) this.init();
|
|
299
|
+
if (!this.overlay || !this.widget) return; // container element not found
|
|
227
300
|
this.overlay.classList.add('active');
|
|
228
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
|
+
}
|
|
229
306
|
this.render();
|
|
230
307
|
if (this.options.onOpen) this.options.onOpen();
|
|
231
308
|
}
|
|
@@ -261,6 +338,15 @@ class BookingWidget {
|
|
|
261
338
|
}
|
|
262
339
|
|
|
263
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
|
+
}
|
|
264
350
|
const doRender = () => {
|
|
265
351
|
if (step !== 'summary' && step !== 'payment') {
|
|
266
352
|
this.checkoutShowPaymentForm = false;
|
|
@@ -321,14 +407,14 @@ class BookingWidget {
|
|
|
321
407
|
}
|
|
322
408
|
|
|
323
409
|
this.container = container;
|
|
324
|
-
|
|
325
|
-
//
|
|
410
|
+
|
|
411
|
+
// Always create the overlay and modal so the error is shown inside the dialog,
|
|
412
|
+
// consistent with the React and Vue behaviour.
|
|
326
413
|
this.overlay = document.createElement('div');
|
|
327
414
|
this.overlay.className = 'booking-widget-overlay';
|
|
328
415
|
this.overlay.addEventListener('click', () => this.close());
|
|
329
416
|
container.appendChild(this.overlay);
|
|
330
417
|
|
|
331
|
-
// Create widget modal
|
|
332
418
|
this.widget = document.createElement('div');
|
|
333
419
|
this.widget.className = 'booking-widget-modal';
|
|
334
420
|
this.widget.innerHTML = `
|
|
@@ -343,35 +429,82 @@ class BookingWidget {
|
|
|
343
429
|
});
|
|
344
430
|
container.appendChild(this.widget);
|
|
345
431
|
|
|
346
|
-
// Apply custom colors
|
|
347
432
|
this.applyColors();
|
|
348
|
-
|
|
349
|
-
// Inject CSS if not already present
|
|
350
433
|
this.injectCSS();
|
|
351
434
|
}
|
|
352
435
|
|
|
353
436
|
applyColors() {
|
|
354
437
|
if (!this.widget) return;
|
|
355
|
-
|
|
356
438
|
const colors = this.options.colors;
|
|
357
439
|
if (!colors) return;
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
+
|
|
471
|
+
if (__bwConfigCache[key]) {
|
|
472
|
+
this._applyApiColors(__bwConfigCache[key]);
|
|
473
|
+
this._configState = 'loaded';
|
|
474
|
+
this.applyColors();
|
|
475
|
+
return Promise.resolve(__bwConfigCache[key]);
|
|
374
476
|
}
|
|
477
|
+
|
|
478
|
+
this._configState = 'loading';
|
|
479
|
+
this.render();
|
|
480
|
+
|
|
481
|
+
const url = 'https://ai.thehotelplanet.com/load-config?apikey=' + encodeURIComponent(key) + '&mode=sandbox';
|
|
482
|
+
this._configPromise = fetch(url)
|
|
483
|
+
.then(function (res) {
|
|
484
|
+
if (!res.ok) throw new Error('Failed to load widget configuration (HTTP ' + res.status + ').');
|
|
485
|
+
return res.json();
|
|
486
|
+
})
|
|
487
|
+
.then(function (data) {
|
|
488
|
+
const apiColors = {};
|
|
489
|
+
if (data.widgetBackground) apiColors.background = data.widgetBackground;
|
|
490
|
+
if (data.widgetTextColor) apiColors.text = data.widgetTextColor;
|
|
491
|
+
if (data.primaryColor) apiColors.primary = data.primaryColor;
|
|
492
|
+
if (data.buttonTextColor) apiColors.primaryText = data.buttonTextColor;
|
|
493
|
+
if (data.widgetCardColor) apiColors.card = data.widgetCardColor;
|
|
494
|
+
__bwConfigCache[key] = apiColors;
|
|
495
|
+
self._applyApiColors(apiColors);
|
|
496
|
+
self._configState = 'loaded';
|
|
497
|
+
self.applyColors();
|
|
498
|
+
self.render();
|
|
499
|
+
return apiColors;
|
|
500
|
+
})
|
|
501
|
+
.catch(function (err) {
|
|
502
|
+
self._configError = err.message || 'Failed to load widget configuration.';
|
|
503
|
+
self._configState = 'error';
|
|
504
|
+
self.render();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
return this._configPromise;
|
|
375
508
|
}
|
|
376
509
|
|
|
377
510
|
injectCSS() {
|
|
@@ -392,6 +525,7 @@ class BookingWidget {
|
|
|
392
525
|
window.__bookingWidgetRendering = true;
|
|
393
526
|
try {
|
|
394
527
|
this.renderStepIndicator();
|
|
528
|
+
this.renderPropertyKeyMessage();
|
|
395
529
|
this.renderStepContent();
|
|
396
530
|
} finally {
|
|
397
531
|
window.__bookingWidgetRendering = false;
|
|
@@ -402,7 +536,9 @@ class BookingWidget {
|
|
|
402
536
|
renderStepIndicator() {
|
|
403
537
|
const el = this.widget.querySelector('.booking-widget-step-indicator');
|
|
404
538
|
if (!el) return;
|
|
405
|
-
|
|
539
|
+
const key = this.options.propertyKey;
|
|
540
|
+
const hasPropertyKey = key != null && key !== '' && (typeof key !== 'string' || key.trim() !== '');
|
|
541
|
+
if (!hasPropertyKey || this._configState !== 'loaded' || this.state.step === 'confirmation') {
|
|
406
542
|
el.innerHTML = '';
|
|
407
543
|
return;
|
|
408
544
|
}
|
|
@@ -422,9 +558,79 @@ class BookingWidget {
|
|
|
422
558
|
el.innerHTML = html;
|
|
423
559
|
}
|
|
424
560
|
|
|
561
|
+
renderPropertyKeyMessage() {
|
|
562
|
+
const el = this.widget.querySelector('.booking-widget-property-key-message');
|
|
563
|
+
if (!el) return;
|
|
564
|
+
// Config errors are now rendered inside renderStepContent via the full error card.
|
|
565
|
+
el.style.display = 'none';
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
_retryConfigFetch() {
|
|
569
|
+
this._configState = 'idle';
|
|
570
|
+
this._configError = null;
|
|
571
|
+
this._configPromise = null;
|
|
572
|
+
this._fetchRuntimeConfig();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
_configErrorHTML(type, title, body, hint) {
|
|
576
|
+
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>`;
|
|
577
|
+
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>`;
|
|
578
|
+
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>`;
|
|
579
|
+
const icon = type === 'missing' ? lockIcon : warnIcon;
|
|
580
|
+
const badgeLabel = type === 'missing'
|
|
581
|
+
? `<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`
|
|
582
|
+
: 'Configuration Error';
|
|
583
|
+
const retryBtn = type === 'fetch'
|
|
584
|
+
? `<button class="booking-widget-config-error__retry" onclick="window.bookingWidgetInstance && window.bookingWidgetInstance._retryConfigFetch()">${retryIcon} Try Again</button>`
|
|
585
|
+
: '';
|
|
586
|
+
return `<div class="booking-widget-config-error" role="alert">
|
|
587
|
+
<div class="booking-widget-config-error__icon-wrap">${icon}</div>
|
|
588
|
+
<span class="booking-widget-config-error__badge">${badgeLabel}</span>
|
|
589
|
+
<h3 class="booking-widget-config-error__title">${escapeHTML(title)}</h3>
|
|
590
|
+
<p class="booking-widget-config-error__desc">${body}</p>
|
|
591
|
+
<div class="booking-widget-config-error__divider"></div>
|
|
592
|
+
<p class="booking-widget-config-error__hint">${escapeHTML(hint)}</p>
|
|
593
|
+
${retryBtn}
|
|
594
|
+
</div>`;
|
|
595
|
+
}
|
|
596
|
+
|
|
425
597
|
// ===== Step Renderers =====
|
|
426
598
|
renderStepContent() {
|
|
427
599
|
const el = this.widget.querySelector('.booking-widget-step-content');
|
|
600
|
+
if (!el) return;
|
|
601
|
+
|
|
602
|
+
// Missing propertyKey — show error card inside the modal, same as React/Vue.
|
|
603
|
+
const key = this.options.propertyKey;
|
|
604
|
+
if (!key || !String(key).trim()) {
|
|
605
|
+
el.innerHTML = this._configErrorHTML(
|
|
606
|
+
'missing',
|
|
607
|
+
'Widget Not Configured',
|
|
608
|
+
'A <code>propertyKey</code> option is required to initialize this booking widget. Please provide it to load rooms and availability.',
|
|
609
|
+
'Contact the site administrator to configure this widget.'
|
|
610
|
+
);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (this._configState === 'loading') {
|
|
615
|
+
el.innerHTML = `<div class="booking-widget-config-loading">
|
|
616
|
+
<div class="booking-widget-config-loading__spinner"></div>
|
|
617
|
+
<span class="booking-widget-config-loading__text">Loading configuration\u2026</span>
|
|
618
|
+
</div>`;
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (this._configState === 'error') {
|
|
622
|
+
el.innerHTML = this._configErrorHTML(
|
|
623
|
+
'fetch',
|
|
624
|
+
'Could Not Load Config',
|
|
625
|
+
escapeHTML(this._configError || 'An unexpected error occurred.'),
|
|
626
|
+
'Please try again or contact support.'
|
|
627
|
+
);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (this._configState !== 'loaded') {
|
|
631
|
+
el.innerHTML = '';
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
428
634
|
switch (this.state.step) {
|
|
429
635
|
case 'dates':
|
|
430
636
|
el.innerHTML = this.renderDatesStep();
|
|
@@ -949,9 +1155,10 @@ class BookingWidget {
|
|
|
949
1155
|
if (this.state.step !== 'payment' || !this.checkoutShowPaymentForm || !this.options.stripePublishableKey || typeof this.options.createPaymentIntent !== 'function') return;
|
|
950
1156
|
const buildPaymentIntentPayload = typeof window !== 'undefined' && window.buildPaymentIntentPayload;
|
|
951
1157
|
const buildCheckoutPayload = typeof window !== 'undefined' && window.buildCheckoutPayload;
|
|
1158
|
+
const sandbox = this.options.mode === 'sandbox';
|
|
952
1159
|
const paymentIntentPayload = buildPaymentIntentPayload
|
|
953
|
-
? buildPaymentIntentPayload(this.state, { propertyKey: this.options.propertyKey ?? undefined })
|
|
954
|
-
: (buildCheckoutPayload ? buildCheckoutPayload(this.state, { propertyId: this.options.propertyId ?? undefined, propertyKey: this.options.propertyKey ?? undefined }) : null);
|
|
1160
|
+
? buildPaymentIntentPayload(this.state, { propertyKey: this.options.propertyKey ?? undefined, sandbox })
|
|
1161
|
+
: (buildCheckoutPayload ? buildCheckoutPayload(this.state, { propertyId: this.options.propertyId ?? undefined, propertyKey: this.options.propertyKey ?? undefined, sandbox }) : null);
|
|
955
1162
|
if (!paymentIntentPayload) return;
|
|
956
1163
|
const self = this;
|
|
957
1164
|
this.paymentElementReady = false;
|
|
@@ -1030,7 +1237,8 @@ class BookingWidget {
|
|
|
1030
1237
|
const canSubmit = this.state.guest.firstName && this.state.guest.lastName && this.state.guest.email;
|
|
1031
1238
|
if (!canSubmit) return;
|
|
1032
1239
|
const buildPayload = typeof window !== 'undefined' && window.buildCheckoutPayload;
|
|
1033
|
-
const
|
|
1240
|
+
const sandbox = this.options.mode === 'sandbox';
|
|
1241
|
+
const payload = buildPayload ? buildPayload(this.state, { propertyId: this.options.propertyId ?? undefined, propertyKey: this.options.propertyKey ?? undefined, sandbox }) : null;
|
|
1034
1242
|
const hasStripe = this.options.stripePublishableKey && typeof this.options.createPaymentIntent === 'function';
|
|
1035
1243
|
|
|
1036
1244
|
// Summary step with Stripe: go to payment step (form loads there)
|
|
@@ -1177,7 +1385,7 @@ class BookingWidget {
|
|
|
1177
1385
|
const token = String(confirmationToken || '').trim();
|
|
1178
1386
|
if (!token) throw new Error('Missing confirmation token');
|
|
1179
1387
|
const base = String(this.options.confirmationBaseUrl || 'https://ai.thehotelplanet.com').replace(/\/$/, '');
|
|
1180
|
-
const url = `${base}/proxy/confirmation/${encodeURIComponent(token)}`;
|
|
1388
|
+
const url = `${base}/proxy/confirmation/${encodeURIComponent(token)}${this.options.mode === 'sandbox' ? '?sandbox=true' : ''}`;
|
|
1181
1389
|
const res = await fetch(url, { method: 'POST' });
|
|
1182
1390
|
if (!res.ok) throw new Error(await res.text());
|
|
1183
1391
|
return await res.json();
|