@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/react/styles.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;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime configuration service for the booking widget.
|
|
3
|
+
*
|
|
4
|
+
* Fetches styling config from /load-config keyed by propertyKey.
|
|
5
|
+
* Results are cached in memory so subsequent calls for the same key are instant.
|
|
6
|
+
* Throws (or rejects) when propertyKey is missing — callers must handle the
|
|
7
|
+
* "locked" state and display an error UI rather than attempting to fall back.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { DEFAULT_COLORS } from '../core/stripe-config.js';
|
|
11
|
+
import { deriveWidgetStyles } from '../core/color-utils.js';
|
|
12
|
+
|
|
13
|
+
const CONFIG_BASE_URL = 'https://ai.thehotelplanet.com/load-config';
|
|
14
|
+
|
|
15
|
+
/** In-memory cache: propertyKey → raw API color object */
|
|
16
|
+
const _configCache = new Map();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Map the /load-config response field names to the internal color keys used
|
|
20
|
+
* throughout the widget.
|
|
21
|
+
*
|
|
22
|
+
* API field → internal key
|
|
23
|
+
* widgetBackground → background
|
|
24
|
+
* widgetTextColor → text
|
|
25
|
+
* primaryColor → primary
|
|
26
|
+
* buttonTextColor → primaryText
|
|
27
|
+
* widgetCardColor → card
|
|
28
|
+
*/
|
|
29
|
+
function mapApiColors(data) {
|
|
30
|
+
const mapped = {};
|
|
31
|
+
if (data.widgetBackground) mapped.background = data.widgetBackground;
|
|
32
|
+
if (data.widgetTextColor) mapped.text = data.widgetTextColor;
|
|
33
|
+
if (data.primaryColor) mapped.primary = data.primaryColor;
|
|
34
|
+
if (data.buttonTextColor) mapped.primaryText = data.buttonTextColor;
|
|
35
|
+
if (data.widgetCardColor) mapped.card = data.widgetCardColor;
|
|
36
|
+
return mapped;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Merge colors following priority (highest → lowest):
|
|
41
|
+
* 1. installerColors (explicit values in the `colors` prop win over everything)
|
|
42
|
+
* 2. mappedApiColors (values fetched from /load-config override package defaults)
|
|
43
|
+
* 3. DEFAULT_COLORS (built-in fallback)
|
|
44
|
+
*
|
|
45
|
+
* @param {object} mappedApiColors - Colors mapped from the API response.
|
|
46
|
+
* @param {object|null} installerColors - Colors passed via the `colors` prop.
|
|
47
|
+
* @returns {object} Resolved color set with all five standard keys.
|
|
48
|
+
*/
|
|
49
|
+
export function mergeColors(mappedApiColors, installerColors) {
|
|
50
|
+
const base = { ...DEFAULT_COLORS, ...(mappedApiColors || {}) };
|
|
51
|
+
if (!installerColors || typeof installerColors !== 'object') return base;
|
|
52
|
+
return {
|
|
53
|
+
background: installerColors.background ?? base.background,
|
|
54
|
+
text: installerColors.text ?? base.text,
|
|
55
|
+
primary: installerColors.primary ?? base.primary,
|
|
56
|
+
primaryText: installerColors.primaryText ?? base.primaryText,
|
|
57
|
+
card: installerColors.card ?? installerColors.background ?? base.card,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fetch runtime styling configuration from the /load-config endpoint.
|
|
63
|
+
*
|
|
64
|
+
* - Throws synchronously (or rejects) when propertyKey is null, undefined, or
|
|
65
|
+
* an empty string. Callers must catch this and render a "Missing Configuration"
|
|
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.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} propertyKey - The hotel property key / API key.
|
|
71
|
+
* @param {object|null} installerColors - Optional installer color overrides.
|
|
72
|
+
* @returns {Promise<{ apiColors: object, colors: object, widgetStyles: object }>}
|
|
73
|
+
*/
|
|
74
|
+
export async function fetchRuntimeConfig(propertyKey, installerColors = null) {
|
|
75
|
+
if (!propertyKey || !String(propertyKey).trim()) {
|
|
76
|
+
throw new Error('propertyKey is required to initialize the booking widget.');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const key = String(propertyKey).trim();
|
|
80
|
+
|
|
81
|
+
if (_configCache.has(key)) {
|
|
82
|
+
const apiColors = _configCache.get(key);
|
|
83
|
+
const colors = mergeColors(apiColors, installerColors);
|
|
84
|
+
return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const url = `${CONFIG_BASE_URL}?apikey=${encodeURIComponent(key)}&mode=sandbox`;
|
|
88
|
+
const res = await fetch(url);
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
throw new Error(`Failed to load widget configuration (HTTP ${res.status}).`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const data = await res.json();
|
|
94
|
+
const apiColors = mapApiColors(data);
|
|
95
|
+
_configCache.set(key, apiColors);
|
|
96
|
+
|
|
97
|
+
const colors = mergeColors(apiColors, installerColors);
|
|
98
|
+
return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors) };
|
|
99
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<button class="booking-widget-close" @click="requestClose">
|
|
11
11
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
|
12
12
|
</button>
|
|
13
|
-
<div v-if="state.step !== 'confirmation'" class="booking-widget-step-indicator">
|
|
13
|
+
<div v-if="hasPropertyKey && configLoaded && state.step !== 'confirmation'" class="booking-widget-step-indicator">
|
|
14
14
|
<template v-for="(step, i) in STEPS" :key="step.key">
|
|
15
15
|
<div class="step-item">
|
|
16
16
|
<span
|
|
@@ -34,7 +34,46 @@
|
|
|
34
34
|
</span>
|
|
35
35
|
</template>
|
|
36
36
|
</div>
|
|
37
|
-
|
|
37
|
+
<!-- Missing propertyKey error -->
|
|
38
|
+
<div v-if="!propertyKey || !String(propertyKey).trim()" class="booking-widget-config-error" role="alert">
|
|
39
|
+
<div class="booking-widget-config-error__icon-wrap">
|
|
40
|
+
<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>
|
|
41
|
+
</div>
|
|
42
|
+
<span class="booking-widget-config-error__badge">
|
|
43
|
+
<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>
|
|
44
|
+
Missing Configuration
|
|
45
|
+
</span>
|
|
46
|
+
<h3 class="booking-widget-config-error__title">Widget Not Configured</h3>
|
|
47
|
+
<p class="booking-widget-config-error__desc">
|
|
48
|
+
A <code>propertyKey</code> prop is required to initialize this booking widget. Please provide it to load rooms and availability.
|
|
49
|
+
</p>
|
|
50
|
+
<div class="booking-widget-config-error__divider"></div>
|
|
51
|
+
<p class="booking-widget-config-error__hint">Contact the site administrator to configure this widget.</p>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<!-- Config fetch error -->
|
|
55
|
+
<div v-else-if="configError" class="booking-widget-config-error" role="alert">
|
|
56
|
+
<div class="booking-widget-config-error__icon-wrap">
|
|
57
|
+
<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>
|
|
58
|
+
</div>
|
|
59
|
+
<span class="booking-widget-config-error__badge">Configuration Error</span>
|
|
60
|
+
<h3 class="booking-widget-config-error__title">Could Not Load Config</h3>
|
|
61
|
+
<p class="booking-widget-config-error__desc">{{ configError }}</p>
|
|
62
|
+
<div class="booking-widget-config-error__divider"></div>
|
|
63
|
+
<p class="booking-widget-config-error__hint">Please try again or contact support.</p>
|
|
64
|
+
<button class="booking-widget-config-error__retry" @click="_initRuntimeConfig()">
|
|
65
|
+
<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>
|
|
66
|
+
Try Again
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- Config loading -->
|
|
71
|
+
<div v-else-if="configLoading" class="booking-widget-config-loading">
|
|
72
|
+
<div class="booking-widget-config-loading__spinner"></div>
|
|
73
|
+
<span class="booking-widget-config-loading__text">Loading configuration…</span>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div v-else class="booking-widget-step-content">
|
|
38
77
|
<!-- Dates Step -->
|
|
39
78
|
<div v-if="state.step === 'dates'">
|
|
40
79
|
<h2 class="step-title">Plan Your Stay</h2>
|
|
@@ -442,6 +481,8 @@ import { loadStripe } from '@stripe/stripe-js';
|
|
|
442
481
|
import '../core/styles.css';
|
|
443
482
|
import { createBookingApi, formatPrice, buildCheckoutPayload, buildPaymentIntentPayload } from '../core/booking-api.js';
|
|
444
483
|
import { STRIPE_PUBLISHABLE_KEY, API_BASE_URL } from '../core/stripe-config.js';
|
|
484
|
+
import { fetchRuntimeConfig } from '../utils/config-service.js';
|
|
485
|
+
|
|
445
486
|
|
|
446
487
|
const BASE_STEPS = [
|
|
447
488
|
{ key: 'dates', label: 'Dates & Guests', num: '01' },
|
|
@@ -467,6 +508,8 @@ export default {
|
|
|
467
508
|
propertyId: { type: [String, Number], default: '' },
|
|
468
509
|
/** Property key/hash for decrypt/pref (pass from your app instead of env). */
|
|
469
510
|
propertyKey: { type: String, default: '' },
|
|
511
|
+
/** 'sandbox' to pass sandbox=true on all proxy/API calls; omit or 'live' to not pass it. */
|
|
512
|
+
mode: { type: String, default: '' },
|
|
470
513
|
availabilityBaseUrl: { type: String, default: '' },
|
|
471
514
|
propertyBaseUrl: { type: String, default: '' },
|
|
472
515
|
s3BaseUrl: { type: String, default: '' },
|
|
@@ -492,6 +535,10 @@ export default {
|
|
|
492
535
|
loadingRates: false,
|
|
493
536
|
apiError: null,
|
|
494
537
|
confirmationCode: null,
|
|
538
|
+
configLoading: false,
|
|
539
|
+
configLoaded: false,
|
|
540
|
+
configError: null,
|
|
541
|
+
runtimeWidgetStyles: {},
|
|
495
542
|
calendarMonth: new Date().getMonth(),
|
|
496
543
|
calendarYear: new Date().getFullYear(),
|
|
497
544
|
pickState: 0,
|
|
@@ -556,9 +603,10 @@ export default {
|
|
|
556
603
|
const url = this.effectivePaymentIntentUrl;
|
|
557
604
|
if (!url) return null;
|
|
558
605
|
return async (payload) => {
|
|
606
|
+
const requestUrl = url + (this.isSandbox ? '?sandbox=true' : '');
|
|
559
607
|
const headers = { 'Content-Type': 'application/json' };
|
|
560
|
-
if (
|
|
561
|
-
const res = await fetch(
|
|
608
|
+
if (requestUrl.includes('ngrok')) headers['ngrok-skip-browser-warning'] = 'true';
|
|
609
|
+
const res = await fetch(requestUrl, { method: 'POST', headers, body: JSON.stringify(payload) });
|
|
562
610
|
if (!res.ok) throw new Error(await res.text());
|
|
563
611
|
const data = await res.json();
|
|
564
612
|
const clientSecret = data.clientSecret ?? data.client_secret ?? data.data?.clientSecret ?? data.data?.client_secret ?? data.paymentIntent?.client_secret;
|
|
@@ -566,6 +614,12 @@ export default {
|
|
|
566
614
|
return { clientSecret, confirmationToken };
|
|
567
615
|
};
|
|
568
616
|
},
|
|
617
|
+
isSandbox() {
|
|
618
|
+
return this.mode === 'sandbox';
|
|
619
|
+
},
|
|
620
|
+
hasPropertyKey() {
|
|
621
|
+
return !!(this.propertyKey != null && this.propertyKey !== '' && String(this.propertyKey).trim() !== '');
|
|
622
|
+
},
|
|
569
623
|
bookingApiRef() {
|
|
570
624
|
if (this.bookingApi && typeof this.bookingApi.fetchRooms === 'function') return this.bookingApi;
|
|
571
625
|
if ((this.effectiveApiBaseUrl || this.propertyKey) && typeof createBookingApi === 'function') {
|
|
@@ -576,29 +630,14 @@ export default {
|
|
|
576
630
|
s3BaseUrl: this.effectiveS3BaseUrl || undefined,
|
|
577
631
|
propertyId: this.propertyId != null && this.propertyId !== '' ? String(this.propertyId) : undefined,
|
|
578
632
|
propertyKey: this.propertyKey || undefined,
|
|
633
|
+
mode: this.mode === 'sandbox' ? 'sandbox' : undefined,
|
|
579
634
|
headers: this.apiSecret ? { 'X-API-Key': this.apiSecret } : undefined,
|
|
580
635
|
});
|
|
581
636
|
}
|
|
582
637
|
return null;
|
|
583
638
|
},
|
|
584
639
|
widgetStyles() {
|
|
585
|
-
|
|
586
|
-
const styles = {};
|
|
587
|
-
if (this.colors.background) {
|
|
588
|
-
styles['--bg'] = this.colors.background;
|
|
589
|
-
styles['--card'] = this.colors.background;
|
|
590
|
-
}
|
|
591
|
-
if (this.colors.text) {
|
|
592
|
-
styles['--fg'] = this.colors.text;
|
|
593
|
-
styles['--card-fg'] = this.colors.text;
|
|
594
|
-
}
|
|
595
|
-
if (this.colors.primary) {
|
|
596
|
-
styles['--primary'] = this.colors.primary;
|
|
597
|
-
}
|
|
598
|
-
if (this.colors.primaryText) {
|
|
599
|
-
styles['--primary-fg'] = this.colors.primaryText;
|
|
600
|
-
}
|
|
601
|
-
return styles;
|
|
640
|
+
return this.runtimeWidgetStyles;
|
|
602
641
|
},
|
|
603
642
|
nights() {
|
|
604
643
|
if (!this.state.checkIn || !this.state.checkOut) return 0;
|
|
@@ -684,7 +723,17 @@ export default {
|
|
|
684
723
|
return this.isOpen && !this.isClosing && this.isReadyForOpen;
|
|
685
724
|
},
|
|
686
725
|
},
|
|
726
|
+
created() {
|
|
727
|
+
this._initRuntimeConfig();
|
|
728
|
+
},
|
|
687
729
|
watch: {
|
|
730
|
+
propertyKey() {
|
|
731
|
+
this._initRuntimeConfig();
|
|
732
|
+
},
|
|
733
|
+
colors: {
|
|
734
|
+
deep: true,
|
|
735
|
+
handler() { this._initRuntimeConfig(); },
|
|
736
|
+
},
|
|
688
737
|
isOpen: {
|
|
689
738
|
handler(open) {
|
|
690
739
|
if (open && this.onOpen) this.onOpen();
|
|
@@ -718,10 +767,31 @@ export default {
|
|
|
718
767
|
},
|
|
719
768
|
},
|
|
720
769
|
methods: {
|
|
770
|
+
async _initRuntimeConfig() {
|
|
771
|
+
if (!this.propertyKey || !String(this.propertyKey).trim()) {
|
|
772
|
+
this.configError = 'propertyKey is required to initialize the booking widget.';
|
|
773
|
+
this.configLoading = false;
|
|
774
|
+
this.configLoaded = false;
|
|
775
|
+
this.runtimeWidgetStyles = {};
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
this.configLoading = true;
|
|
779
|
+
this.configError = null;
|
|
780
|
+
this.configLoaded = false;
|
|
781
|
+
try {
|
|
782
|
+
const { widgetStyles } = await fetchRuntimeConfig(this.propertyKey, this.colors);
|
|
783
|
+
this.runtimeWidgetStyles = widgetStyles;
|
|
784
|
+
this.configLoaded = true;
|
|
785
|
+
} catch (err) {
|
|
786
|
+
this.configError = err?.message || 'Failed to load widget configuration.';
|
|
787
|
+
} finally {
|
|
788
|
+
this.configLoading = false;
|
|
789
|
+
}
|
|
790
|
+
},
|
|
721
791
|
async fetchConfirmationDetails(token) {
|
|
722
792
|
const t = String(token || '').trim();
|
|
723
793
|
if (!t) throw new Error('Missing confirmation token');
|
|
724
|
-
const url = `${this.effectiveConfirmationBaseUrl}/proxy/confirmation/${encodeURIComponent(t)}`;
|
|
794
|
+
const url = `${this.effectiveConfirmationBaseUrl}/proxy/confirmation/${encodeURIComponent(t)}${this.isSandbox ? '?sandbox=true' : ''}`;
|
|
725
795
|
const res = await fetch(url, { method: 'POST' });
|
|
726
796
|
if (!res.ok) throw new Error(await res.text());
|
|
727
797
|
return await res.json();
|
|
@@ -750,7 +820,7 @@ export default {
|
|
|
750
820
|
},
|
|
751
821
|
async loadStripePaymentElement() {
|
|
752
822
|
if (this.state.step !== 'payment' || !this.checkoutShowPaymentForm || !this.stripePublishableKey || typeof this.effectiveCreatePaymentIntent !== 'function') return;
|
|
753
|
-
const paymentIntentPayload = buildPaymentIntentPayload(this.state, { propertyKey: this.propertyKey || undefined });
|
|
823
|
+
const paymentIntentPayload = buildPaymentIntentPayload(this.state, { propertyKey: this.propertyKey || undefined, sandbox: this.isSandbox });
|
|
754
824
|
this.paymentElementReady = false;
|
|
755
825
|
try {
|
|
756
826
|
await this.$nextTick();
|
|
@@ -789,6 +859,8 @@ export default {
|
|
|
789
859
|
return i === ci ? 'active' : i < ci ? 'past' : 'future';
|
|
790
860
|
},
|
|
791
861
|
goToStep(step) {
|
|
862
|
+
// Block navigation to room/rate steps until runtime config is loaded.
|
|
863
|
+
if ((step === 'rooms' || step === 'rates') && !this.configLoaded) return;
|
|
792
864
|
if (step !== 'summary' && step !== 'payment') this.checkoutShowPaymentForm = false;
|
|
793
865
|
if (step === 'payment') this.checkoutShowPaymentForm = true;
|
|
794
866
|
this.state.step = step;
|
|
@@ -859,7 +931,7 @@ export default {
|
|
|
859
931
|
},
|
|
860
932
|
confirmReservation() {
|
|
861
933
|
if (!this.canSubmit) return;
|
|
862
|
-
const payload = buildCheckoutPayload(this.state, { propertyId: this.propertyId != null && this.propertyId !== '' ? this.propertyId : undefined, propertyKey: this.propertyKey || undefined });
|
|
934
|
+
const payload = buildCheckoutPayload(this.state, { propertyId: this.propertyId != null && this.propertyId !== '' ? this.propertyId : undefined, propertyKey: this.propertyKey || undefined, sandbox: this.isSandbox });
|
|
863
935
|
// Summary step with Stripe: go to payment step (form loads there)
|
|
864
936
|
if (this.state.step === 'summary' && this.stripePublishableKey && typeof this.effectiveCreatePaymentIntent === 'function') {
|
|
865
937
|
this.apiError = null;
|
package/dist/vue/styles.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuitee/booking-widget",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
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",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"build": "node scripts/generate-stripe-config.js && npm run build:css && npm run build:js && npm run build:react && npm run build:vue && npm run build:types",
|
|
35
35
|
"build:stripe-config": "node scripts/generate-stripe-config.js",
|
|
36
36
|
"build:css": "mkdir -p dist dist/core && cp src/core/styles.css dist/booking-widget.css && cp src/core/styles.css dist/core/styles.css",
|
|
37
|
-
"build:js": "mkdir -p dist dist/core && cp src/core/widget.js dist/booking-widget.js && cp src/core/booking-api.js dist/core/booking-api.js && cp src/core/stripe-config.js dist/core/stripe-config.js && node scripts/inject-widget-bootstrap.js && cp src/standalone/bundle.js dist/booking-widget-standalone.js && node scripts/build-standalone.js && npm run build:entry",
|
|
37
|
+
"build:js": "mkdir -p dist dist/core dist/utils && cp src/core/widget.js dist/booking-widget.js && cp src/core/booking-api.js dist/core/booking-api.js && cp src/core/stripe-config.js dist/core/stripe-config.js && cp src/core/color-utils.js dist/core/color-utils.js && cp src/utils/config-service.js dist/utils/config-service.js && node scripts/inject-widget-bootstrap.js && cp src/standalone/bundle.js dist/booking-widget-standalone.js && node scripts/build-standalone.js && npm run build:entry",
|
|
38
38
|
"build:react": "mkdir -p dist/react && cp src/react/BookingWidget.jsx dist/react/BookingWidget.jsx && cp src/core/styles.css dist/react/styles.css",
|
|
39
39
|
"build:vue": "mkdir -p dist/vue && cp src/vue/BookingWidget.vue dist/vue/BookingWidget.vue && cp src/core/styles.css dist/vue/styles.css",
|
|
40
40
|
"build:types": "npm run build:types:main && npm run build:types:react && npm run build:types:vue",
|