@nuitee/booking-widget 1.0.3 → 1.0.5

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/README.md CHANGED
@@ -94,15 +94,15 @@ widget.open();
94
94
  No bundler: load the script and CSS from the CDN, then create the widget.
95
95
 
96
96
  ```html
97
- <link rel="stylesheet" href="https://cdn.thehotelplanet.com/booking-widget/v1.0.0/dist/booking-widget.css">
98
- <script src="https://cdn.thehotelplanet.com/booking-widget/v1.0.0/dist/booking-widget-standalone.js"></script>
97
+ <link rel="stylesheet" href="https://cdn.thehotelplanet.com/booking-widget/v1.0.5/dist/booking-widget.css">
98
+ <script src="https://cdn.thehotelplanet.com/booking-widget/v1.0.5/dist/booking-widget-standalone.js"></script>
99
99
 
100
100
  <div id="booking-widget-container"></div>
101
101
 
102
102
  <script>
103
103
  const widget = new BookingWidget({
104
104
  containerId: 'booking-widget-container',
105
- cssUrl: 'https://cdn.thehotelplanet.com/booking-widget/v1.0.0/dist/booking-widget.css',
105
+ cssUrl: 'https://cdn.thehotelplanet.com/booking-widget/v1.0.5/dist/booking-widget.css',
106
106
  propertyKey: 'your-property-key',
107
107
  onOpen: () => console.log('Opened'),
108
108
  onClose: () => console.log('Closed'),
@@ -872,6 +872,7 @@ if (typeof window !== 'undefined') {
872
872
  var styles = { '--primary': primary, '--primary-fg': primaryFg, '--bg': bg, '--fg': fg, '--card-fg': fg };
873
873
  if (primaryRgb) styles['--primary-rgb'] = primaryRgb[0] + ', ' + primaryRgb[1] + ', ' + primaryRgb[2];
874
874
  styles['--card'] = c.card || (bgHsl ? 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 2) + '%)' : bg);
875
+ styles['--card-solid'] = styles['--card'];
875
876
  if (bgHsl) {
876
877
  styles['--secondary'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 10) + '%)';
877
878
  styles['--border'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 14) + '%)';
@@ -1189,18 +1190,21 @@ if (typeof window !== 'undefined') {
1189
1190
  _fetchRuntimeConfig() {
1190
1191
  const self = this;
1191
1192
  const key = String(this.options.propertyKey).trim();
1193
+ const isSandbox = this.options.mode === 'sandbox';
1194
+ // Cache key is mode-aware so sandbox and live configs are stored separately.
1195
+ const cacheKey = isSandbox ? key + ':sandbox' : key;
1192
1196
 
1193
- if (__bwConfigCache[key]) {
1194
- this._applyApiColors(__bwConfigCache[key]);
1197
+ if (__bwConfigCache[cacheKey]) {
1198
+ this._applyApiColors(__bwConfigCache[cacheKey]);
1195
1199
  this._configState = 'loaded';
1196
1200
  this.applyColors();
1197
- return Promise.resolve(__bwConfigCache[key]);
1201
+ return Promise.resolve(__bwConfigCache[cacheKey]);
1198
1202
  }
1199
1203
 
1200
1204
  this._configState = 'loading';
1201
1205
  this.render();
1202
1206
 
1203
- const url = 'https://ai.thehotelplanet.com/load-config?apikey=' + encodeURIComponent(key) + '&mode=sandbox';
1207
+ const url = 'https://ai.thehotelplanet.com/load-config?apikey=' + encodeURIComponent(key) + (isSandbox ? '&mode=sandbox' : '');
1204
1208
  this._configPromise = fetch(url)
1205
1209
  .then(function (res) {
1206
1210
  if (!res.ok) throw new Error('Failed to load widget configuration (HTTP ' + res.status + ').');
@@ -1213,7 +1217,7 @@ if (typeof window !== 'undefined') {
1213
1217
  if (data.primaryColor) apiColors.primary = data.primaryColor;
1214
1218
  if (data.buttonTextColor) apiColors.primaryText = data.buttonTextColor;
1215
1219
  if (data.widgetCardColor) apiColors.card = data.widgetCardColor;
1216
- __bwConfigCache[key] = apiColors;
1220
+ __bwConfigCache[cacheKey] = apiColors;
1217
1221
  self._applyApiColors(apiColors);
1218
1222
  self._configState = 'loaded';
1219
1223
  self.applyColors();
@@ -1259,17 +1263,24 @@ if (typeof window !== 'undefined') {
1259
1263
  }
1260
1264
 
1261
1265
  injectCSS() {
1262
- if (document.getElementById('booking-widget-styles')) return;
1263
-
1264
- if (this.options.cssUrl) {
1265
- const link = document.createElement('link');
1266
- link.id = 'booking-widget-styles';
1267
- link.rel = 'stylesheet';
1268
- link.href = this.options.cssUrl;
1269
- document.head.appendChild(link);
1270
- } else {
1271
- // Inline CSS would go here in a real bundle
1272
- console.warn('CSS not loaded. Please include booking-widget.css or provide cssUrl option.');
1266
+ if (!document.getElementById('booking-widget-styles')) {
1267
+ if (this.options.cssUrl) {
1268
+ const link = document.createElement('link');
1269
+ link.id = 'booking-widget-styles';
1270
+ link.rel = 'stylesheet';
1271
+ link.href = this.options.cssUrl;
1272
+ document.head.appendChild(link);
1273
+ } else {
1274
+ // Inline CSS would go here in a real bundle
1275
+ console.warn('CSS not loaded. Please include booking-widget.css or provide cssUrl option.');
1276
+ }
1277
+ }
1278
+
1279
+ if (!document.getElementById('booking-widget-overrides')) {
1280
+ const style = document.createElement('style');
1281
+ style.id = 'booking-widget-overrides';
1282
+ style.textContent = '.booking-widget-modal .confirm-icon { font-size: 1em; }';
1283
+ document.head.appendChild(style);
1273
1284
  }
1274
1285
  }
1275
1286
 
@@ -1491,10 +1502,17 @@ if (typeof window !== 'undefined') {
1491
1502
  this.render();
1492
1503
  }
1493
1504
 
1494
- initCalendar() {
1495
- const now = new Date();
1496
- this.calendarMonth = now.getMonth();
1497
- this.calendarYear = now.getFullYear();
1505
+ // force=true resets the view when the user opens the picker.
1506
+ // - If a check-in date is already selected, starts from that month/year.
1507
+ // - Otherwise starts from today.
1508
+ // force=false (default) preserves the current month/year so mid-pick re-renders
1509
+ // don't snap the calendar back to the current month.
1510
+ initCalendar(force = false) {
1511
+ if (force || this.calendarMonth === null || this.calendarYear === null) {
1512
+ const anchor = this.state.checkIn || new Date();
1513
+ this.calendarMonth = anchor.getMonth();
1514
+ this.calendarYear = anchor.getFullYear();
1515
+ }
1498
1516
  // If we have check-in but no check-out, next click sets check-out; otherwise next click sets check-in (allows changing check-in when both are set)
1499
1517
  this.pickState = (this.state.checkIn && !this.state.checkOut) ? 1 : 0;
1500
1518
  }
@@ -1503,7 +1521,7 @@ if (typeof window !== 'undefined') {
1503
1521
  const popup = this.widget.querySelector('.calendar-popup');
1504
1522
  popup.classList.toggle('open');
1505
1523
  if (popup.classList.contains('open')) {
1506
- this.initCalendar();
1524
+ this.initCalendar(true);
1507
1525
  this.renderCalendar();
1508
1526
  }
1509
1527
  }
@@ -1559,10 +1577,14 @@ if (typeof window !== 'undefined') {
1559
1577
 
1560
1578
  pickDate(y, m, d) {
1561
1579
  const date = new Date(y, m, d);
1580
+ // Save the current view so render() cannot shift the calendar.
1581
+ const savedMonth = this.calendarMonth;
1582
+ const savedYear = this.calendarYear;
1562
1583
  if (this.pickState === 0 || (this.state.checkIn && date <= this.state.checkIn)) {
1563
1584
  this.state.checkIn = date; this.state.checkOut = null; this.pickState = 1;
1564
1585
  this.render();
1565
- this.initCalendar();
1586
+ // Restore view after render (render calls initCalendar which may reset month/year).
1587
+ if (savedMonth !== null) { this.calendarMonth = savedMonth; this.calendarYear = savedYear; }
1566
1588
  this.pickState = 1;
1567
1589
  const popup = this.widget.querySelector('.calendar-popup');
1568
1590
  if (popup) popup.classList.add('open');
@@ -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%);
@@ -611,7 +612,7 @@
611
612
  max-width: calc(100vw - 2em);
612
613
  box-sizing: border-box;
613
614
  z-index: 10;
614
- background: var(--card-solid, var(--card));
615
+ background: var(--card-solid);
615
616
  border: 1px solid var(--border);
616
617
  border-radius: var(--radius);
617
618
  padding: 1em;
@@ -84,6 +84,7 @@ function deriveWidgetStyles(c) {
84
84
  var styles = { '--primary': primary, '--primary-fg': primaryFg, '--bg': bg, '--fg': fg, '--card-fg': fg };
85
85
  if (primaryRgb) styles['--primary-rgb'] = primaryRgb[0] + ', ' + primaryRgb[1] + ', ' + primaryRgb[2];
86
86
  styles['--card'] = c.card || (bgHsl ? 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 2) + '%)' : bg);
87
+ styles['--card-solid'] = styles['--card'];
87
88
  if (bgHsl) {
88
89
  styles['--secondary'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 10) + '%)';
89
90
  styles['--border'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 14) + '%)';
@@ -467,18 +468,21 @@ class BookingWidget {
467
468
  const self = this;
468
469
  const propertyKey = this.options.propertyKey;
469
470
  const key = String(propertyKey).trim();
471
+ const isSandbox = this.options.mode === 'sandbox';
472
+ // Cache key is mode-aware so sandbox and live configs are stored separately.
473
+ const cacheKey = isSandbox ? key + ':sandbox' : key;
470
474
 
471
- if (__bwConfigCache[key]) {
472
- this._applyApiColors(__bwConfigCache[key]);
475
+ if (__bwConfigCache[cacheKey]) {
476
+ this._applyApiColors(__bwConfigCache[cacheKey]);
473
477
  this._configState = 'loaded';
474
478
  this.applyColors();
475
- return Promise.resolve(__bwConfigCache[key]);
479
+ return Promise.resolve(__bwConfigCache[cacheKey]);
476
480
  }
477
481
 
478
482
  this._configState = 'loading';
479
483
  this.render();
480
484
 
481
- const url = 'https://ai.thehotelplanet.com/load-config?apikey=' + encodeURIComponent(key) + '&mode=sandbox';
485
+ const url = 'https://ai.thehotelplanet.com/load-config?apikey=' + encodeURIComponent(key) + (isSandbox ? '&mode=sandbox' : '');
482
486
  this._configPromise = fetch(url)
483
487
  .then(function (res) {
484
488
  if (!res.ok) throw new Error('Failed to load widget configuration (HTTP ' + res.status + ').');
@@ -491,7 +495,7 @@ class BookingWidget {
491
495
  if (data.primaryColor) apiColors.primary = data.primaryColor;
492
496
  if (data.buttonTextColor) apiColors.primaryText = data.buttonTextColor;
493
497
  if (data.widgetCardColor) apiColors.card = data.widgetCardColor;
494
- __bwConfigCache[key] = apiColors;
498
+ __bwConfigCache[cacheKey] = apiColors;
495
499
  self._applyApiColors(apiColors);
496
500
  self._configState = 'loaded';
497
501
  self.applyColors();
@@ -508,13 +512,20 @@ class BookingWidget {
508
512
  }
509
513
 
510
514
  injectCSS() {
511
- if (document.getElementById('booking-widget-styles')) return;
512
-
513
- const link = document.createElement('link');
514
- link.id = 'booking-widget-styles';
515
- link.rel = 'stylesheet';
516
- link.href = this.options.cssUrl || './booking-widget.css';
517
- document.head.appendChild(link);
515
+ if (!document.getElementById('booking-widget-styles')) {
516
+ const link = document.createElement('link');
517
+ link.id = 'booking-widget-styles';
518
+ link.rel = 'stylesheet';
519
+ link.href = this.options.cssUrl || './booking-widget.css';
520
+ document.head.appendChild(link);
521
+ }
522
+
523
+ if (!document.getElementById('booking-widget-overrides')) {
524
+ const style = document.createElement('style');
525
+ style.id = 'booking-widget-overrides';
526
+ style.textContent = '.booking-widget-modal .confirm-icon { font-size: 1em; }';
527
+ document.head.appendChild(style);
528
+ }
518
529
  }
519
530
 
520
531
  // ===== Render =====
@@ -760,10 +771,17 @@ class BookingWidget {
760
771
  }
761
772
 
762
773
  // --- Calendar ---
763
- initCalendar() {
764
- const now = new Date();
765
- this.calendarMonth = now.getMonth();
766
- this.calendarYear = now.getFullYear();
774
+ // force=true resets the view when the user opens the picker.
775
+ // - If a check-in date is already selected, starts from that month/year.
776
+ // - Otherwise starts from today.
777
+ // force=false (default) preserves the current month/year so mid-pick re-renders
778
+ // don't snap the calendar back to the current month.
779
+ initCalendar(force = false) {
780
+ if (force || this.calendarMonth === null || this.calendarYear === null) {
781
+ const anchor = this.state.checkIn || new Date();
782
+ this.calendarMonth = anchor.getMonth();
783
+ this.calendarYear = anchor.getFullYear();
784
+ }
767
785
  // If we have check-in but no check-out, next click sets check-out; otherwise next click sets check-in (allows changing check-in when both are set)
768
786
  this.pickState = (this.state.checkIn && !this.state.checkOut) ? 1 : 0;
769
787
  }
@@ -772,7 +790,7 @@ class BookingWidget {
772
790
  const popup = this.widget.querySelector('.calendar-popup');
773
791
  popup.classList.toggle('open');
774
792
  if (popup.classList.contains('open')) {
775
- this.initCalendar();
793
+ this.initCalendar(true);
776
794
  this.renderCalendar();
777
795
  }
778
796
  }
@@ -828,6 +846,9 @@ class BookingWidget {
828
846
 
829
847
  pickDate(y, m, d) {
830
848
  const date = new Date(y, m, d);
849
+ // Save the current view so render() cannot shift the calendar.
850
+ const savedMonth = this.calendarMonth;
851
+ const savedYear = this.calendarYear;
831
852
  if (this.pickState === 0 || (this.state.checkIn && date <= this.state.checkIn)) {
832
853
  this.state.checkIn = date; this.state.checkOut = null; this.pickState = 1;
833
854
  } else {
@@ -835,9 +856,9 @@ class BookingWidget {
835
856
  this.widget.querySelector('.calendar-popup').classList.remove('open');
836
857
  }
837
858
  this.render();
859
+ // Restore view after render (render calls initCalendar which may reset month/year).
860
+ if (savedMonth !== null) { this.calendarMonth = savedMonth; this.calendarYear = savedYear; }
838
861
  if (this.pickState === 1) {
839
- this.initCalendar();
840
- this.pickState = 1;
841
862
  const popup = this.widget.querySelector('.calendar-popup');
842
863
  if (popup) popup.classList.add('open');
843
864
  this.renderCalendar();
@@ -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%);
@@ -611,7 +612,7 @@
611
612
  max-width: calc(100vw - 2em);
612
613
  box-sizing: border-box;
613
614
  z-index: 10;
614
- background: var(--card-solid, var(--card));
615
+ background: var(--card-solid);
615
616
  border: 1px solid var(--border);
616
617
  border-radius: var(--radius);
617
618
  padding: 1em;
@@ -240,7 +240,7 @@ const BookingWidget = ({
240
240
  setConfigLoading(true);
241
241
  setConfigError(null);
242
242
  setConfigLoaded(false);
243
- fetchRuntimeConfig(propertyKey, colors)
243
+ fetchRuntimeConfig(propertyKey, colors, mode)
244
244
  .then(({ widgetStyles }) => {
245
245
  if (cancelled) return;
246
246
  setRuntimeWidgetStyles(widgetStyles);
@@ -253,7 +253,7 @@ const BookingWidget = ({
253
253
  setConfigLoading(false);
254
254
  });
255
255
  return () => { cancelled = true; };
256
- }, [propertyKey, colors, configRetryCount]);
256
+ }, [propertyKey, colors, mode, configRetryCount]);
257
257
 
258
258
  // Apply resolved CSS custom properties to the widget element.
259
259
  useEffect(() => {
@@ -399,8 +399,6 @@ const BookingWidget = ({
399
399
  setCalendarOpen(false);
400
400
  }
401
401
  if (pickState === 0) {
402
- setCalendarMonth(new Date().getMonth());
403
- setCalendarYear(new Date().getFullYear());
404
402
  setPickState(1);
405
403
  }
406
404
  };
@@ -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%);
@@ -611,7 +612,7 @@
611
612
  max-width: calc(100vw - 2em);
612
613
  box-sizing: border-box;
613
614
  z-index: 10;
614
- background: var(--card-solid, var(--card));
615
+ background: var(--card-solid);
615
616
  border: 1px solid var(--border);
616
617
  border-radius: var(--radius);
617
618
  padding: 1em;
@@ -64,27 +64,31 @@ export function mergeColors(mappedApiColors, installerColors) {
64
64
  * - Throws synchronously (or rejects) when propertyKey is null, undefined, or
65
65
  * an empty string. Callers must catch this and render a "Missing Configuration"
66
66
  * error state rather than attempting to render the normal widget UI.
67
- * - Caches the raw API color payload by propertyKey. Subsequent calls for the
68
- * same key re-use the cache and only re-merge installer overrides.
67
+ * - Caches the raw API color payload by propertyKey + mode. Subsequent calls for
68
+ * the same key+mode re-use the cache and only re-merge installer overrides.
69
69
  *
70
- * @param {string} propertyKey - The hotel property key / API key.
71
- * @param {object|null} installerColors - Optional installer color overrides.
70
+ * @param {string} propertyKey - The hotel property key / API key.
71
+ * @param {object|null} installerColors - Optional installer color overrides.
72
+ * @param {string} [mode] - Pass 'sandbox' to append &mode=sandbox to the request.
72
73
  * @returns {Promise<{ apiColors: object, colors: object, widgetStyles: object }>}
73
74
  */
74
- export async function fetchRuntimeConfig(propertyKey, installerColors = null) {
75
+ export async function fetchRuntimeConfig(propertyKey, installerColors = null, mode = null) {
75
76
  if (!propertyKey || !String(propertyKey).trim()) {
76
77
  throw new Error('propertyKey is required to initialize the booking widget.');
77
78
  }
78
79
 
79
80
  const key = String(propertyKey).trim();
81
+ const isSandbox = mode === 'sandbox';
82
+ // Cache separately for sandbox vs live so switching modes gets a fresh fetch.
83
+ const cacheKey = isSandbox ? `${key}:sandbox` : key;
80
84
 
81
- if (_configCache.has(key)) {
82
- const apiColors = _configCache.get(key);
85
+ if (_configCache.has(cacheKey)) {
86
+ const apiColors = _configCache.get(cacheKey);
83
87
  const colors = mergeColors(apiColors, installerColors);
84
88
  return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors) };
85
89
  }
86
90
 
87
- const url = `${CONFIG_BASE_URL}?apikey=${encodeURIComponent(key)}&mode=sandbox`;
91
+ const url = `${CONFIG_BASE_URL}?apikey=${encodeURIComponent(key)}${isSandbox ? '&mode=sandbox' : ''}`;
88
92
  const res = await fetch(url);
89
93
  if (!res.ok) {
90
94
  throw new Error(`Failed to load widget configuration (HTTP ${res.status}).`);
@@ -92,7 +96,7 @@ export async function fetchRuntimeConfig(propertyKey, installerColors = null) {
92
96
 
93
97
  const data = await res.json();
94
98
  const apiColors = mapApiColors(data);
95
- _configCache.set(key, apiColors);
99
+ _configCache.set(cacheKey, apiColors);
96
100
 
97
101
  const colors = mergeColors(apiColors, installerColors);
98
102
  return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors) };
@@ -730,6 +730,9 @@ export default {
730
730
  propertyKey() {
731
731
  this._initRuntimeConfig();
732
732
  },
733
+ mode() {
734
+ this._initRuntimeConfig();
735
+ },
733
736
  colors: {
734
737
  deep: true,
735
738
  handler() { this._initRuntimeConfig(); },
@@ -779,7 +782,7 @@ export default {
779
782
  this.configError = null;
780
783
  this.configLoaded = false;
781
784
  try {
782
- const { widgetStyles } = await fetchRuntimeConfig(this.propertyKey, this.colors);
785
+ const { widgetStyles } = await fetchRuntimeConfig(this.propertyKey, this.colors, this.mode);
783
786
  this.runtimeWidgetStyles = widgetStyles;
784
787
  this.configLoaded = true;
785
788
  } catch (err) {
@@ -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%);
@@ -611,7 +612,7 @@
611
612
  max-width: calc(100vw - 2em);
612
613
  box-sizing: border-box;
613
614
  z-index: 10;
614
- background: var(--card-solid, var(--card));
615
+ background: var(--card-solid);
615
616
  border: 1px solid var(--border);
616
617
  border-radius: var(--radius);
617
618
  padding: 1em;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuitee/booking-widget",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "A beautiful, customizable booking widget modal that can be embedded in any website. Supports vanilla JavaScript, React, and Vue.js.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",