@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/USAGE.md
CHANGED
|
@@ -254,7 +254,7 @@ Only the following props/options are for you to set. API URLs, Stripe, and booki
|
|
|
254
254
|
| `onComplete` | function | Called when the booking is completed, with booking data. |
|
|
255
255
|
| `onOpen` | function | Optional. Called when the widget opens. |
|
|
256
256
|
| `propertyKey` | string | Property key for real rooms/rates/booking; omit for demo data. |
|
|
257
|
-
| `colors` | object | Optional. `{ background, text, primary, primaryText }`. |
|
|
257
|
+
| `colors` | object | Optional. Pass 4 colors: `{ background, text, primary, primaryText }`. Card, muted, border, etc. are derived. Add `card` to override. |
|
|
258
258
|
|
|
259
259
|
### Vanilla constructor options
|
|
260
260
|
|
|
@@ -266,7 +266,7 @@ Only the following props/options are for you to set. API URLs, Stripe, and booki
|
|
|
266
266
|
| `onClose` | function | Called when the user closes the widget. |
|
|
267
267
|
| `onComplete` | function | Called when the booking is completed. |
|
|
268
268
|
| `onOpen` | function | Optional. Called when the widget opens. |
|
|
269
|
-
| `colors` | object | Optional. `{ background, text, primary, primaryText }`. |
|
|
269
|
+
| `colors` | object | Optional. Pass 4 colors: `{ background, text, primary, primaryText }`. Card, muted, border, etc. are derived. Add `card` to override. |
|
|
270
270
|
|
|
271
271
|
### Vanilla methods
|
|
272
272
|
|
|
@@ -286,4 +286,4 @@ Only the following props/options are for you to set. API URLs, Stripe, and booki
|
|
|
286
286
|
| Use in vanilla (no build) | Load the standalone script and CSS, `new BookingWidget({ containerId, propertyKey, ... })`, then `init()` and `open()`. |
|
|
287
287
|
| Use in vanilla (with build) | Import `@nuitee/booking-widget` and `@nuitee/booking-widget/css`, pass `propertyKey` and callbacks, then `open()`. |
|
|
288
288
|
| Real rooms/rates/booking | Pass `propertyKey`. The widget uses its own runtime config for API and payments. |
|
|
289
|
-
| Custom colors | Pass `colors: { background, text, primary, primaryText }`. |
|
|
289
|
+
| Custom colors | Pass `colors: { background, text, primary, primaryText }`. Card, muted, border, input-bg derive from these. |
|
|
@@ -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
|
/**
|
|
3
3
|
* Shared Booking API layer for the @nuitee/booking-widget.
|
|
4
4
|
* Used by Vanilla JS, Vue, and React variants so integration lives in one place.
|
|
@@ -619,6 +619,16 @@ function buildCheckoutPayload(state, options = {}) {
|
|
|
619
619
|
* @param {Object} [options] - { propertyKey: string, sandbox?: boolean }
|
|
620
620
|
* @returns {{ rate_identifier: string, key: string, metadata: Object, sandbox?: boolean }}
|
|
621
621
|
*/
|
|
622
|
+
function generateUUID() {
|
|
623
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
624
|
+
return crypto.randomUUID();
|
|
625
|
+
}
|
|
626
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
627
|
+
const r = Math.random() * 16 | 0;
|
|
628
|
+
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
622
632
|
function buildPaymentIntentPayload(state, options = {}) {
|
|
623
633
|
const rawKey = options.propertyKey != null && options.propertyKey !== '' ? String(options.propertyKey).trim() : '';
|
|
624
634
|
const propertyKey = rawKey && rawKey.startsWith('book_engine_') ? rawKey : '';
|
|
@@ -658,6 +668,7 @@ function buildPaymentIntentPayload(state, options = {}) {
|
|
|
658
668
|
email: g.email ?? '',
|
|
659
669
|
occupancies: occupanciesForPayload,
|
|
660
670
|
source_transaction: 'booking engine',
|
|
671
|
+
booking_code: generateUUID(),
|
|
661
672
|
},
|
|
662
673
|
};
|
|
663
674
|
if (sandbox) out.sandbox = true;
|
|
@@ -828,10 +839,64 @@ if (typeof window !== 'undefined') {
|
|
|
828
839
|
return `<span class="icon" style="display:inline-flex;align-items:center;justify-content:center;width:${size};height:${size};min-width:${size};min-height:${size};vertical-align:middle;flex-shrink:0;color:inherit;">${sizedIcon}</span>`;
|
|
829
840
|
}
|
|
830
841
|
|
|
831
|
-
|
|
832
|
-
|
|
842
|
+
function deriveWidgetStyles(c) {
|
|
843
|
+
if (!c || (!c.background && !c.text && !c.primary && !c.primaryText)) return {};
|
|
844
|
+
function expandHex(hex) {
|
|
845
|
+
if (!hex || typeof hex !== 'string') return null;
|
|
846
|
+
var h = hex.replace(/^#/, '').trim();
|
|
847
|
+
if (h.length === 3) return '#' + h.split('').map(function (x) { return x + x; }).join('');
|
|
848
|
+
return h.length === 6 ? '#' + h : null;
|
|
849
|
+
}
|
|
850
|
+
function hexToRgb(hex) {
|
|
851
|
+
var x = expandHex(hex);
|
|
852
|
+
if (!x) return null;
|
|
853
|
+
return [parseInt(x.slice(1, 3), 16), parseInt(x.slice(3, 5), 16), parseInt(x.slice(5, 7), 16)];
|
|
854
|
+
}
|
|
855
|
+
function hexToHsl(hex) {
|
|
856
|
+
var x = expandHex(hex);
|
|
857
|
+
if (!x) return null;
|
|
858
|
+
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;
|
|
859
|
+
var max = Math.max(r, g, b), min = Math.min(r, g, b), h, s, l = (max + min) / 2;
|
|
860
|
+
if (max === min) h = s = 0;
|
|
861
|
+
else {
|
|
862
|
+
var d = max - min;
|
|
863
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
864
|
+
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
865
|
+
else if (max === g) h = ((b - r) / d + 2) / 6;
|
|
866
|
+
else h = ((r - g) / d + 4) / 6;
|
|
867
|
+
}
|
|
868
|
+
return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
|
|
869
|
+
}
|
|
870
|
+
var bg = c.background || '#1a1a1a', fg = c.text || '#e0e0e0', primary = c.primary || '#3b82f6', primaryFg = c.primaryText || '#ffffff';
|
|
871
|
+
var primaryRgb = hexToRgb(primary), bgHsl = hexToHsl(bg), fgHsl = hexToHsl(fg);
|
|
872
|
+
var styles = { '--primary': primary, '--primary-fg': primaryFg, '--bg': bg, '--fg': fg, '--card-fg': fg };
|
|
873
|
+
if (primaryRgb) styles['--primary-rgb'] = primaryRgb[0] + ', ' + primaryRgb[1] + ', ' + primaryRgb[2];
|
|
874
|
+
styles['--card'] = c.card || (bgHsl ? 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 2) + '%)' : bg);
|
|
875
|
+
if (bgHsl) {
|
|
876
|
+
styles['--secondary'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 10) + '%)';
|
|
877
|
+
styles['--border'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 14) + '%)';
|
|
878
|
+
styles['--input-bg'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 6) + '%)';
|
|
879
|
+
}
|
|
880
|
+
if (fgHsl) {
|
|
881
|
+
styles['--secondary-fg'] = 'hsl(' + fgHsl[0] + ', ' + fgHsl[1] + '%, ' + Math.max(20, fgHsl[2] - 10) + '%)';
|
|
882
|
+
styles['--muted'] = 'hsl(' + fgHsl[0] + ', ' + fgHsl[1] + '%, ' + Math.max(30, fgHsl[2] - 25) + '%)';
|
|
883
|
+
}
|
|
884
|
+
styles['--font-serif'] = "'Playfair Display', Georgia, serif";
|
|
885
|
+
styles['--font-sans'] = "'Inter', system-ui, sans-serif";
|
|
886
|
+
styles['--radius'] = '0.75rem';
|
|
887
|
+
return styles;
|
|
888
|
+
}
|
|
889
|
+
|
|
833
890
|
const builtInStripeKey = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_STRIPE_KEY__) ? window.__BOOKING_WIDGET_STRIPE_KEY__ : '';
|
|
834
891
|
|
|
892
|
+
function escapeHTML(value) {
|
|
893
|
+
const s = String(value ?? '');
|
|
894
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// In-memory config cache shared across all instances, keyed by propertyKey.
|
|
898
|
+
const __bwConfigCache = {};
|
|
899
|
+
|
|
835
900
|
class BookingWidget {
|
|
836
901
|
constructor(options = {}) {
|
|
837
902
|
const defaultApiBase = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_API_BASE_URL__) ? window.__BOOKING_WIDGET_API_BASE_URL__ : '';
|
|
@@ -869,14 +934,20 @@ if (typeof window !== 'undefined') {
|
|
|
869
934
|
mode: options.mode || null,
|
|
870
935
|
bookingApi: options.bookingApi || null,
|
|
871
936
|
cssUrl: options.cssUrl || null,
|
|
872
|
-
// Color customization
|
|
873
|
-
colors: {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
937
|
+
// Color customization: CONFIG defaults from load-config, installer colors override
|
|
938
|
+
colors: (function () {
|
|
939
|
+
const dc = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_DEFAULT_COLORS__) ? window.__BOOKING_WIDGET_DEFAULT_COLORS__ : {};
|
|
940
|
+
const inst = options.colors && typeof options.colors === 'object' ? options.colors : {};
|
|
941
|
+
return {
|
|
942
|
+
background: inst.background ?? dc.background ?? null,
|
|
943
|
+
text: inst.text ?? dc.text ?? null,
|
|
944
|
+
primary: inst.primary ?? dc.primary ?? null,
|
|
945
|
+
primaryText: inst.primaryText ?? dc.primaryText ?? null,
|
|
946
|
+
// When card omitted, use installer's background for consistency
|
|
947
|
+
card: inst.card ?? inst.background ?? dc.card ?? dc.background ?? null,
|
|
948
|
+
...inst
|
|
949
|
+
};
|
|
950
|
+
})(),
|
|
880
951
|
...options
|
|
881
952
|
};
|
|
882
953
|
|
|
@@ -930,6 +1001,15 @@ if (typeof window !== 'undefined') {
|
|
|
930
1001
|
this.container = null;
|
|
931
1002
|
this.overlay = null;
|
|
932
1003
|
this.widget = null;
|
|
1004
|
+
|
|
1005
|
+
// Store raw installer colors for re-merging after API config fetch.
|
|
1006
|
+
this._rawInstallerColors = options.colors && typeof options.colors === 'object'
|
|
1007
|
+
? Object.assign({}, options.colors)
|
|
1008
|
+
: {};
|
|
1009
|
+
// Config fetch state: 'idle' | 'loading' | 'loaded' | 'error'
|
|
1010
|
+
this._configState = 'idle';
|
|
1011
|
+
this._configError = null;
|
|
1012
|
+
this._configPromise = null;
|
|
933
1013
|
}
|
|
934
1014
|
|
|
935
1015
|
getNights() {
|
|
@@ -966,8 +1046,13 @@ if (typeof window !== 'undefined') {
|
|
|
966
1046
|
|
|
967
1047
|
open() {
|
|
968
1048
|
if (!this.container) this.init();
|
|
1049
|
+
if (!this.overlay || !this.widget) return; // container element not found
|
|
969
1050
|
this.overlay.classList.add('active');
|
|
970
1051
|
this.widget.classList.add('active');
|
|
1052
|
+
// Only fetch config when propertyKey is present; missing-key is rendered inside the modal.
|
|
1053
|
+
if (this.options.propertyKey && String(this.options.propertyKey).trim() && this._configState === 'idle') {
|
|
1054
|
+
this._fetchRuntimeConfig();
|
|
1055
|
+
}
|
|
971
1056
|
this.render();
|
|
972
1057
|
if (this.options.onOpen) this.options.onOpen();
|
|
973
1058
|
}
|
|
@@ -994,6 +1079,12 @@ if (typeof window !== 'undefined') {
|
|
|
994
1079
|
}
|
|
995
1080
|
|
|
996
1081
|
goToStep(step) {
|
|
1082
|
+
if ((step === 'rooms' || step === 'rates') && this._configState !== 'loaded') {
|
|
1083
|
+
if (this._configState === 'loading' && this._configPromise) {
|
|
1084
|
+
this._configPromise.then(() => { if (this._configState === 'loaded') this.goToStep(step); });
|
|
1085
|
+
}
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
997
1088
|
if (step !== 'summary' && step !== 'payment') {
|
|
998
1089
|
this.checkoutShowPaymentForm = false;
|
|
999
1090
|
this.paymentElementReady = false;
|
|
@@ -1045,7 +1136,9 @@ if (typeof window !== 'undefined') {
|
|
|
1045
1136
|
}
|
|
1046
1137
|
|
|
1047
1138
|
this.container = container;
|
|
1048
|
-
|
|
1139
|
+
|
|
1140
|
+
// Always create the overlay and modal so the error is shown inside the dialog,
|
|
1141
|
+
// consistent with the React and Vue behaviour.
|
|
1049
1142
|
this.overlay = document.createElement('div');
|
|
1050
1143
|
this.overlay.className = 'booking-widget-overlay';
|
|
1051
1144
|
this.overlay.addEventListener('click', () => this.close());
|
|
@@ -1056,44 +1149,116 @@ if (typeof window !== 'undefined') {
|
|
|
1056
1149
|
this.widget.innerHTML = `
|
|
1057
1150
|
<button class="booking-widget-close">${iconHTML('x', '1.5em')}</button>
|
|
1058
1151
|
<div class="booking-widget-step-indicator"></div>
|
|
1059
|
-
<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>
|
|
1060
1152
|
<div class="booking-widget-step-content"></div>
|
|
1061
1153
|
`;
|
|
1062
1154
|
this.widget.querySelector('.booking-widget-close').addEventListener('click', () => this.close());
|
|
1155
|
+
this.widget.querySelector('.booking-widget-step-indicator').addEventListener('click', (e) => {
|
|
1156
|
+
const stepEl = e.target.closest('[data-step]');
|
|
1157
|
+
if (stepEl && stepEl.dataset.step) this.goToStep(stepEl.dataset.step);
|
|
1158
|
+
});
|
|
1063
1159
|
container.appendChild(this.widget);
|
|
1064
1160
|
|
|
1065
1161
|
if (typeof window !== 'undefined') {
|
|
1066
1162
|
window.bookingWidgetInstance = this;
|
|
1067
1163
|
}
|
|
1068
1164
|
|
|
1069
|
-
// Apply custom colors
|
|
1070
1165
|
this.applyColors();
|
|
1071
|
-
|
|
1072
1166
|
this.injectCSS();
|
|
1073
1167
|
}
|
|
1074
1168
|
|
|
1075
1169
|
applyColors() {
|
|
1076
1170
|
if (!this.widget) return;
|
|
1077
|
-
|
|
1078
1171
|
const colors = this.options.colors;
|
|
1079
1172
|
if (!colors) return;
|
|
1080
|
-
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1173
|
+
const styles = deriveWidgetStyles(colors);
|
|
1174
|
+
const el = this.widget.style;
|
|
1175
|
+
for (var k in styles) if (styles[k] != null && styles[k] !== '') el.setProperty(k, styles[k]);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
_applyApiColors(apiColors) {
|
|
1179
|
+
const inst = this._rawInstallerColors || {};
|
|
1180
|
+
this.options.colors = {
|
|
1181
|
+
background: inst.background != null ? inst.background : (apiColors.background || null),
|
|
1182
|
+
text: inst.text != null ? inst.text : (apiColors.text || null),
|
|
1183
|
+
primary: inst.primary != null ? inst.primary : (apiColors.primary || null),
|
|
1184
|
+
primaryText: inst.primaryText != null ? inst.primaryText : (apiColors.primaryText || null),
|
|
1185
|
+
card: inst.card != null ? inst.card : (inst.background != null ? inst.background : (apiColors.card || null)),
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
_fetchRuntimeConfig() {
|
|
1190
|
+
const self = this;
|
|
1191
|
+
const key = String(this.options.propertyKey).trim();
|
|
1192
|
+
const isSandbox = this.options.mode === 'sandbox';
|
|
1193
|
+
// Cache key is mode-aware so sandbox and live configs are stored separately.
|
|
1194
|
+
const cacheKey = isSandbox ? key + ':sandbox' : key;
|
|
1195
|
+
|
|
1196
|
+
if (__bwConfigCache[cacheKey]) {
|
|
1197
|
+
this._applyApiColors(__bwConfigCache[cacheKey]);
|
|
1198
|
+
this._configState = 'loaded';
|
|
1199
|
+
this.applyColors();
|
|
1200
|
+
return Promise.resolve(__bwConfigCache[cacheKey]);
|
|
1096
1201
|
}
|
|
1202
|
+
|
|
1203
|
+
this._configState = 'loading';
|
|
1204
|
+
this.render();
|
|
1205
|
+
|
|
1206
|
+
const url = 'https://ai.thehotelplanet.com/load-config?apikey=' + encodeURIComponent(key) + (isSandbox ? '&mode=sandbox' : '');
|
|
1207
|
+
this._configPromise = fetch(url)
|
|
1208
|
+
.then(function (res) {
|
|
1209
|
+
if (!res.ok) throw new Error('Failed to load widget configuration (HTTP ' + res.status + ').');
|
|
1210
|
+
return res.json();
|
|
1211
|
+
})
|
|
1212
|
+
.then(function (data) {
|
|
1213
|
+
const apiColors = {};
|
|
1214
|
+
if (data.widgetBackground) apiColors.background = data.widgetBackground;
|
|
1215
|
+
if (data.widgetTextColor) apiColors.text = data.widgetTextColor;
|
|
1216
|
+
if (data.primaryColor) apiColors.primary = data.primaryColor;
|
|
1217
|
+
if (data.buttonTextColor) apiColors.primaryText = data.buttonTextColor;
|
|
1218
|
+
if (data.widgetCardColor) apiColors.card = data.widgetCardColor;
|
|
1219
|
+
__bwConfigCache[cacheKey] = apiColors;
|
|
1220
|
+
self._applyApiColors(apiColors);
|
|
1221
|
+
self._configState = 'loaded';
|
|
1222
|
+
self.applyColors();
|
|
1223
|
+
self.render();
|
|
1224
|
+
return apiColors;
|
|
1225
|
+
})
|
|
1226
|
+
.catch(function (err) {
|
|
1227
|
+
self._configError = err.message || 'Failed to load widget configuration.';
|
|
1228
|
+
self._configState = 'error';
|
|
1229
|
+
self.render();
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
return this._configPromise;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
_retryConfigFetch() {
|
|
1236
|
+
this._configState = 'idle';
|
|
1237
|
+
this._configError = null;
|
|
1238
|
+
this._configPromise = null;
|
|
1239
|
+
this._fetchRuntimeConfig();
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
_configErrorHTML(type, title, body, hint) {
|
|
1243
|
+
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>`;
|
|
1244
|
+
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>`;
|
|
1245
|
+
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>`;
|
|
1246
|
+
const icon = type === 'missing' ? lockIcon : warnIcon;
|
|
1247
|
+
const badgeLabel = type === 'missing'
|
|
1248
|
+
? `<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`
|
|
1249
|
+
: 'Configuration Error';
|
|
1250
|
+
const retryBtn = type === 'fetch'
|
|
1251
|
+
? `<button class="booking-widget-config-error__retry" onclick="window.bookingWidgetInstance && window.bookingWidgetInstance._retryConfigFetch()">${retryIcon} Try Again</button>`
|
|
1252
|
+
: '';
|
|
1253
|
+
return `<div class="booking-widget-config-error" role="alert">
|
|
1254
|
+
<div class="booking-widget-config-error__icon-wrap">${icon}</div>
|
|
1255
|
+
<span class="booking-widget-config-error__badge">${badgeLabel}</span>
|
|
1256
|
+
<h3 class="booking-widget-config-error__title">${escapeHTML(title)}</h3>
|
|
1257
|
+
<p class="booking-widget-config-error__desc">${body}</p>
|
|
1258
|
+
<div class="booking-widget-config-error__divider"></div>
|
|
1259
|
+
<p class="booking-widget-config-error__hint">${escapeHTML(hint)}</p>
|
|
1260
|
+
${retryBtn}
|
|
1261
|
+
</div>`;
|
|
1097
1262
|
}
|
|
1098
1263
|
|
|
1099
1264
|
injectCSS() {
|
|
@@ -1118,16 +1283,7 @@ if (typeof window !== 'undefined') {
|
|
|
1118
1283
|
}
|
|
1119
1284
|
|
|
1120
1285
|
renderPropertyKeyMessage() {
|
|
1121
|
-
|
|
1122
|
-
if (!el) return;
|
|
1123
|
-
const key = this.options.propertyKey;
|
|
1124
|
-
const missing = !key || (typeof key === 'string' && !key.trim());
|
|
1125
|
-
if (missing) {
|
|
1126
|
-
el.style.display = 'block';
|
|
1127
|
-
el.innerHTML = 'The propertyKey is missing. Please provide it as a prop to load rooms from the API.';
|
|
1128
|
-
} else {
|
|
1129
|
-
el.style.display = 'none';
|
|
1130
|
-
}
|
|
1286
|
+
// Config errors are rendered directly inside renderStepContent as full cards.
|
|
1131
1287
|
}
|
|
1132
1288
|
|
|
1133
1289
|
renderStepIndicator() {
|
|
@@ -1135,7 +1291,7 @@ if (typeof window !== 'undefined') {
|
|
|
1135
1291
|
if (!el) return;
|
|
1136
1292
|
const key = this.options.propertyKey;
|
|
1137
1293
|
const hasPropertyKey = key != null && key !== '' && (typeof key !== 'string' || key.trim() !== '');
|
|
1138
|
-
if (!hasPropertyKey || this.state.step === 'confirmation') {
|
|
1294
|
+
if (!hasPropertyKey || this._configState !== 'loaded' || this.state.step === 'confirmation') {
|
|
1139
1295
|
el.innerHTML = '';
|
|
1140
1296
|
return;
|
|
1141
1297
|
}
|
|
@@ -1158,9 +1314,36 @@ if (typeof window !== 'undefined') {
|
|
|
1158
1314
|
renderStepContent() {
|
|
1159
1315
|
const el = this.widget.querySelector('.booking-widget-step-content');
|
|
1160
1316
|
if (!el) return;
|
|
1317
|
+
|
|
1318
|
+
// Missing propertyKey — show error card inside the modal, same as React/Vue.
|
|
1161
1319
|
const key = this.options.propertyKey;
|
|
1162
|
-
|
|
1163
|
-
|
|
1320
|
+
if (!key || !String(key).trim()) {
|
|
1321
|
+
el.innerHTML = this._configErrorHTML(
|
|
1322
|
+
'missing',
|
|
1323
|
+
'Widget Not Configured',
|
|
1324
|
+
'A <code>propertyKey</code> option is required to initialize this booking widget. Please provide it to load rooms and availability.',
|
|
1325
|
+
'Contact the site administrator to configure this widget.'
|
|
1326
|
+
);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
if (this._configState === 'loading') {
|
|
1331
|
+
el.innerHTML = `<div class="booking-widget-config-loading">
|
|
1332
|
+
<div class="booking-widget-config-loading__spinner"></div>
|
|
1333
|
+
<span class="booking-widget-config-loading__text">Loading configuration\u2026</span>
|
|
1334
|
+
</div>`;
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
if (this._configState === 'error') {
|
|
1338
|
+
el.innerHTML = this._configErrorHTML(
|
|
1339
|
+
'fetch',
|
|
1340
|
+
'Could Not Load Config',
|
|
1341
|
+
escapeHTML(this._configError || 'An unexpected error occurred.'),
|
|
1342
|
+
'Please try again or contact support.'
|
|
1343
|
+
);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
if (this._configState !== 'loaded') {
|
|
1164
1347
|
el.innerHTML = '';
|
|
1165
1348
|
return;
|
|
1166
1349
|
}
|
package/dist/booking-widget.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;
|