@nuitee/booking-widget 1.0.4 → 1.0.6

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.6/dist/booking-widget.css">
98
+ <script src="https://cdn.thehotelplanet.com/booking-widget/v1.0.6/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.6/dist/booking-widget.css',
106
106
  propertyKey: 'your-property-key',
107
107
  onOpen: () => console.log('Opened'),
108
108
  onClose: () => console.log('Closed'),
@@ -230,6 +230,7 @@ function createBookingApi(config = {}) {
230
230
  const url = `${(base || '').replace(/\/$/, '')}${path}`;
231
231
  const headers = {
232
232
  'Content-Type': 'application/json',
233
+ 'Source': 'booking_engine',
233
234
  ...staticHeaders,
234
235
  ...getHeaders(),
235
236
  };
@@ -291,8 +292,8 @@ function createBookingApi(config = {}) {
291
292
  ? `${propertyBaseUrl}/ari/get_properties?id=${encodeURIComponent(propertyId)}`
292
293
  : `/ari/get_properties?id=${encodeURIComponent(propertyId)}`;
293
294
  try {
294
- const getOpts = { method: 'GET' };
295
- if (propFullUrl.includes('ngrok')) getOpts.headers = { 'ngrok-skip-browser-warning': 'true' };
295
+ const getOpts = { method: 'GET', headers: { 'Source': 'booking_engine' } };
296
+ if (propFullUrl.includes('ngrok')) getOpts.headers['ngrok-skip-browser-warning'] = 'true';
296
297
  const propRes = await fetch(propFullUrl, getOpts);
297
298
  if (propRes.ok) {
298
299
  const propData = await propRes.json();
@@ -692,7 +693,7 @@ async function decryptPropertyId(options = {}) {
692
693
  const url = `${base}/${locale}/booking_engine/decrypt_property_id`;
693
694
  const res = await fetch(url, {
694
695
  method: 'POST',
695
- headers: { 'Content-Type': 'application/json', ...headers },
696
+ headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
696
697
  body: JSON.stringify({ hash: propertyKey }),
697
698
  });
698
699
  if (!res.ok) throw new Error(res.statusText || 'Failed to decrypt property id');
@@ -719,7 +720,7 @@ async function fetchBookingEnginePref(options = {}) {
719
720
  const url = `${base}/api/core/${locale}/booking_engine/booking_engine_pref?property_id=${encodeURIComponent(propertyId)}`;
720
721
  const res = await fetch(url, {
721
722
  method: 'GET',
722
- headers: { 'Content-Type': 'application/json', ...headers },
723
+ headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
723
724
  });
724
725
  if (!res.ok) throw new Error(res.statusText || 'Failed to fetch config');
725
726
 
@@ -872,6 +873,7 @@ if (typeof window !== 'undefined') {
872
873
  var styles = { '--primary': primary, '--primary-fg': primaryFg, '--bg': bg, '--fg': fg, '--card-fg': fg };
873
874
  if (primaryRgb) styles['--primary-rgb'] = primaryRgb[0] + ', ' + primaryRgb[1] + ', ' + primaryRgb[2];
874
875
  styles['--card'] = c.card || (bgHsl ? 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 2) + '%)' : bg);
876
+ styles['--card-solid'] = styles['--card'];
875
877
  if (bgHsl) {
876
878
  styles['--secondary'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 10) + '%)';
877
879
  styles['--border'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 14) + '%)';
@@ -905,7 +907,7 @@ if (typeof window !== 'undefined') {
905
907
  const url = defaultApiBase.replace(/\/$/, '') + '/proxy/create-payment-intent' + (options.mode === 'sandbox' ? '?sandbox=true' : '');
906
908
  return fetch(url, {
907
909
  method: 'POST',
908
- headers: { 'Content-Type': 'application/json' },
910
+ headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine' },
909
911
  body: JSON.stringify(payload),
910
912
  }).then(function (r) {
911
913
  if (!r.ok) throw new Error(r.statusText || 'Payment intent failed');
@@ -1204,7 +1206,7 @@ if (typeof window !== 'undefined') {
1204
1206
  this.render();
1205
1207
 
1206
1208
  const url = 'https://ai.thehotelplanet.com/load-config?apikey=' + encodeURIComponent(key) + (isSandbox ? '&mode=sandbox' : '');
1207
- this._configPromise = fetch(url)
1209
+ this._configPromise = fetch(url, { headers: { 'Source': 'booking_engine' } })
1208
1210
  .then(function (res) {
1209
1211
  if (!res.ok) throw new Error('Failed to load widget configuration (HTTP ' + res.status + ').');
1210
1212
  return res.json();
@@ -1262,17 +1264,24 @@ if (typeof window !== 'undefined') {
1262
1264
  }
1263
1265
 
1264
1266
  injectCSS() {
1265
- if (document.getElementById('booking-widget-styles')) return;
1266
-
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.');
1267
+ if (!document.getElementById('booking-widget-styles')) {
1268
+ if (this.options.cssUrl) {
1269
+ const link = document.createElement('link');
1270
+ link.id = 'booking-widget-styles';
1271
+ link.rel = 'stylesheet';
1272
+ link.href = this.options.cssUrl;
1273
+ document.head.appendChild(link);
1274
+ } else {
1275
+ // Inline CSS would go here in a real bundle
1276
+ console.warn('CSS not loaded. Please include booking-widget.css or provide cssUrl option.');
1277
+ }
1278
+ }
1279
+
1280
+ if (!document.getElementById('booking-widget-overrides')) {
1281
+ const style = document.createElement('style');
1282
+ style.id = 'booking-widget-overrides';
1283
+ style.textContent = '.booking-widget-modal .confirm-icon { font-size: 1em; }';
1284
+ document.head.appendChild(style);
1276
1285
  }
1277
1286
  }
1278
1287
 
@@ -1494,10 +1503,17 @@ if (typeof window !== 'undefined') {
1494
1503
  this.render();
1495
1504
  }
1496
1505
 
1497
- initCalendar() {
1498
- const now = new Date();
1499
- this.calendarMonth = now.getMonth();
1500
- this.calendarYear = now.getFullYear();
1506
+ // force=true resets the view when the user opens the picker.
1507
+ // - If a check-in date is already selected, starts from that month/year.
1508
+ // - Otherwise starts from today.
1509
+ // force=false (default) preserves the current month/year so mid-pick re-renders
1510
+ // don't snap the calendar back to the current month.
1511
+ initCalendar(force = false) {
1512
+ if (force || this.calendarMonth === null || this.calendarYear === null) {
1513
+ const anchor = this.state.checkIn || new Date();
1514
+ this.calendarMonth = anchor.getMonth();
1515
+ this.calendarYear = anchor.getFullYear();
1516
+ }
1501
1517
  // 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)
1502
1518
  this.pickState = (this.state.checkIn && !this.state.checkOut) ? 1 : 0;
1503
1519
  }
@@ -1506,7 +1522,7 @@ if (typeof window !== 'undefined') {
1506
1522
  const popup = this.widget.querySelector('.calendar-popup');
1507
1523
  popup.classList.toggle('open');
1508
1524
  if (popup.classList.contains('open')) {
1509
- this.initCalendar();
1525
+ this.initCalendar(true);
1510
1526
  this.renderCalendar();
1511
1527
  }
1512
1528
  }
@@ -1562,10 +1578,14 @@ if (typeof window !== 'undefined') {
1562
1578
 
1563
1579
  pickDate(y, m, d) {
1564
1580
  const date = new Date(y, m, d);
1581
+ // Save the current view so render() cannot shift the calendar.
1582
+ const savedMonth = this.calendarMonth;
1583
+ const savedYear = this.calendarYear;
1565
1584
  if (this.pickState === 0 || (this.state.checkIn && date <= this.state.checkIn)) {
1566
1585
  this.state.checkIn = date; this.state.checkOut = null; this.pickState = 1;
1567
1586
  this.render();
1568
- this.initCalendar();
1587
+ // Restore view after render (render calls initCalendar which may reset month/year).
1588
+ if (savedMonth !== null) { this.calendarMonth = savedMonth; this.calendarYear = savedYear; }
1569
1589
  this.pickState = 1;
1570
1590
  const popup = this.widget.querySelector('.calendar-popup');
1571
1591
  if (popup) popup.classList.add('open');
@@ -1972,7 +1992,7 @@ if (typeof window !== 'undefined') {
1972
1992
  if (!token) throw new Error('Missing confirmation token');
1973
1993
  const base = String(this.options.confirmationBaseUrl || 'https://ai.thehotelplanet.com').replace(/\/$/, '');
1974
1994
  const url = base + '/proxy/confirmation/' + encodeURIComponent(token) + (this.options.mode === 'sandbox' ? '?sandbox=true' : '');
1975
- const res = await fetch(url, { method: 'POST' });
1995
+ const res = await fetch(url, { method: 'POST', headers: { 'Source': 'booking_engine' } });
1976
1996
  if (!res.ok) throw new Error(await res.text());
1977
1997
  return await res.json();
1978
1998
  }
@@ -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) + '%)';
@@ -121,7 +122,7 @@ class BookingWidget {
121
122
  const url = apiBase.replace(/\/$/, '') + '/proxy/create-payment-intent' + (builtInMode === 'sandbox' ? '?sandbox=true' : '');
122
123
  return fetch(url, {
123
124
  method: 'POST',
124
- headers: { 'Content-Type': 'application/json' },
125
+ headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine' },
125
126
  body: JSON.stringify(payload),
126
127
  }).then(function (r) {
127
128
  if (!r.ok) throw new Error(r.statusText || 'Payment intent failed');
@@ -482,7 +483,7 @@ class BookingWidget {
482
483
  this.render();
483
484
 
484
485
  const url = 'https://ai.thehotelplanet.com/load-config?apikey=' + encodeURIComponent(key) + (isSandbox ? '&mode=sandbox' : '');
485
- this._configPromise = fetch(url)
486
+ this._configPromise = fetch(url, { headers: { 'Source': 'booking_engine' } })
486
487
  .then(function (res) {
487
488
  if (!res.ok) throw new Error('Failed to load widget configuration (HTTP ' + res.status + ').');
488
489
  return res.json();
@@ -511,13 +512,20 @@ class BookingWidget {
511
512
  }
512
513
 
513
514
  injectCSS() {
514
- if (document.getElementById('booking-widget-styles')) return;
515
-
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);
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
+ }
521
529
  }
522
530
 
523
531
  // ===== Render =====
@@ -763,10 +771,17 @@ class BookingWidget {
763
771
  }
764
772
 
765
773
  // --- Calendar ---
766
- initCalendar() {
767
- const now = new Date();
768
- this.calendarMonth = now.getMonth();
769
- 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
+ }
770
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)
771
786
  this.pickState = (this.state.checkIn && !this.state.checkOut) ? 1 : 0;
772
787
  }
@@ -775,7 +790,7 @@ class BookingWidget {
775
790
  const popup = this.widget.querySelector('.calendar-popup');
776
791
  popup.classList.toggle('open');
777
792
  if (popup.classList.contains('open')) {
778
- this.initCalendar();
793
+ this.initCalendar(true);
779
794
  this.renderCalendar();
780
795
  }
781
796
  }
@@ -831,6 +846,9 @@ class BookingWidget {
831
846
 
832
847
  pickDate(y, m, d) {
833
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;
834
852
  if (this.pickState === 0 || (this.state.checkIn && date <= this.state.checkIn)) {
835
853
  this.state.checkIn = date; this.state.checkOut = null; this.pickState = 1;
836
854
  } else {
@@ -838,9 +856,9 @@ class BookingWidget {
838
856
  this.widget.querySelector('.calendar-popup').classList.remove('open');
839
857
  }
840
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; }
841
861
  if (this.pickState === 1) {
842
- this.initCalendar();
843
- this.pickState = 1;
844
862
  const popup = this.widget.querySelector('.calendar-popup');
845
863
  if (popup) popup.classList.add('open');
846
864
  this.renderCalendar();
@@ -1389,7 +1407,7 @@ class BookingWidget {
1389
1407
  if (!token) throw new Error('Missing confirmation token');
1390
1408
  const base = String(this.options.confirmationBaseUrl || 'https://ai.thehotelplanet.com').replace(/\/$/, '');
1391
1409
  const url = `${base}/proxy/confirmation/${encodeURIComponent(token)}${this.options.mode === 'sandbox' ? '?sandbox=true' : ''}`;
1392
- const res = await fetch(url, { method: 'POST' });
1410
+ const res = await fetch(url, { method: 'POST', headers: { 'Source': 'booking_engine' } });
1393
1411
  if (!res.ok) throw new Error(await res.text());
1394
1412
  return await res.json();
1395
1413
  }
@@ -229,6 +229,7 @@ function createBookingApi(config = {}) {
229
229
  const url = `${(base || '').replace(/\/$/, '')}${path}`;
230
230
  const headers = {
231
231
  'Content-Type': 'application/json',
232
+ 'Source': 'booking_engine',
232
233
  ...staticHeaders,
233
234
  ...getHeaders(),
234
235
  };
@@ -290,8 +291,8 @@ function createBookingApi(config = {}) {
290
291
  ? `${propertyBaseUrl}/ari/get_properties?id=${encodeURIComponent(propertyId)}`
291
292
  : `/ari/get_properties?id=${encodeURIComponent(propertyId)}`;
292
293
  try {
293
- const getOpts = { method: 'GET' };
294
- if (propFullUrl.includes('ngrok')) getOpts.headers = { 'ngrok-skip-browser-warning': 'true' };
294
+ const getOpts = { method: 'GET', headers: { 'Source': 'booking_engine' } };
295
+ if (propFullUrl.includes('ngrok')) getOpts.headers['ngrok-skip-browser-warning'] = 'true';
295
296
  const propRes = await fetch(propFullUrl, getOpts);
296
297
  if (propRes.ok) {
297
298
  const propData = await propRes.json();
@@ -691,7 +692,7 @@ async function decryptPropertyId(options = {}) {
691
692
  const url = `${base}/${locale}/booking_engine/decrypt_property_id`;
692
693
  const res = await fetch(url, {
693
694
  method: 'POST',
694
- headers: { 'Content-Type': 'application/json', ...headers },
695
+ headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
695
696
  body: JSON.stringify({ hash: propertyKey }),
696
697
  });
697
698
  if (!res.ok) throw new Error(res.statusText || 'Failed to decrypt property id');
@@ -718,7 +719,7 @@ async function fetchBookingEnginePref(options = {}) {
718
719
  const url = `${base}/api/core/${locale}/booking_engine/booking_engine_pref?property_id=${encodeURIComponent(propertyId)}`;
719
720
  const res = await fetch(url, {
720
721
  method: 'GET',
721
- headers: { 'Content-Type': 'application/json', ...headers },
722
+ headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
722
723
  });
723
724
  if (!res.ok) throw new Error(res.statusText || 'Failed to fetch config');
724
725
 
@@ -57,7 +57,7 @@ const BookingWidget = ({
57
57
  if (!effectivePaymentIntentUrl) return null;
58
58
  return async (payload) => {
59
59
  const url = effectivePaymentIntentUrl + (isSandbox ? '?sandbox=true' : '');
60
- const headers = { 'Content-Type': 'application/json' };
60
+ const headers = { 'Content-Type': 'application/json', 'Source': 'booking_engine' };
61
61
  if (url.includes('ngrok')) headers['ngrok-skip-browser-warning'] = 'true';
62
62
  const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(payload) });
63
63
  if (!res.ok) throw new Error(await res.text());
@@ -205,7 +205,7 @@ const BookingWidget = ({
205
205
  if (cancelled) return;
206
206
  try {
207
207
  const url = `${effectiveConfirmationBaseUrl}/proxy/confirmation/${encodeURIComponent(String(confirmationToken).trim())}${isSandbox ? '?sandbox=true' : ''}`;
208
- const res = await fetch(url, { method: 'POST' });
208
+ const res = await fetch(url, { method: 'POST', headers: { 'Source': 'booking_engine' } });
209
209
  if (!res.ok) throw new Error(await res.text());
210
210
  const data = await res.json();
211
211
  const status = data?.status != null ? String(data.status) : '';
@@ -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
  };
@@ -89,7 +89,7 @@ export async function fetchRuntimeConfig(propertyKey, installerColors = null, mo
89
89
  }
90
90
 
91
91
  const url = `${CONFIG_BASE_URL}?apikey=${encodeURIComponent(key)}${isSandbox ? '&mode=sandbox' : ''}`;
92
- const res = await fetch(url);
92
+ const res = await fetch(url, { headers: { 'Source': 'booking_engine' } });
93
93
  if (!res.ok) {
94
94
  throw new Error(`Failed to load widget configuration (HTTP ${res.status}).`);
95
95
  }
@@ -604,7 +604,7 @@ export default {
604
604
  if (!url) return null;
605
605
  return async (payload) => {
606
606
  const requestUrl = url + (this.isSandbox ? '?sandbox=true' : '');
607
- const headers = { 'Content-Type': 'application/json' };
607
+ const headers = { 'Content-Type': 'application/json', 'Source': 'booking_engine' };
608
608
  if (requestUrl.includes('ngrok')) headers['ngrok-skip-browser-warning'] = 'true';
609
609
  const res = await fetch(requestUrl, { method: 'POST', headers, body: JSON.stringify(payload) });
610
610
  if (!res.ok) throw new Error(await res.text());
@@ -795,7 +795,7 @@ export default {
795
795
  const t = String(token || '').trim();
796
796
  if (!t) throw new Error('Missing confirmation token');
797
797
  const url = `${this.effectiveConfirmationBaseUrl}/proxy/confirmation/${encodeURIComponent(t)}${this.isSandbox ? '?sandbox=true' : ''}`;
798
- const res = await fetch(url, { method: 'POST' });
798
+ const res = await fetch(url, { method: 'POST', headers: { 'Source': 'booking_engine' } });
799
799
  if (!res.ok) throw new Error(await res.text());
800
800
  return await res.json();
801
801
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuitee/booking-widget",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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",