@nuitee/booking-widget 1.0.2 → 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 +225 -45
- package/dist/booking-widget.css +141 -2
- package/dist/booking-widget.js +221 -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 +141 -2
- package/dist/react/BookingWidget.jsx +92 -29
- package/dist/react/styles.css +141 -2
- package/dist/utils/config-service.js +99 -0
- package/dist/vue/BookingWidget.vue +79 -20
- package/dist/vue/styles.css +141 -2
- package/package.json +2 -2
|
@@ -4,6 +4,7 @@ import { Calendar, Users, User, Check, MapPin, Phone, Square, CreditCard, Lock,
|
|
|
4
4
|
import '../core/styles.css';
|
|
5
5
|
import { createBookingApi, formatPrice, buildCheckoutPayload, buildPaymentIntentPayload } from '../core/booking-api.js';
|
|
6
6
|
import { STRIPE_PUBLISHABLE_KEY, API_BASE_URL } from '../core/stripe-config.js';
|
|
7
|
+
import { fetchRuntimeConfig } from '../utils/config-service.js';
|
|
7
8
|
|
|
8
9
|
const BASE_STEPS = [
|
|
9
10
|
{ key: 'dates', label: 'Dates & Guests', num: '01' },
|
|
@@ -98,6 +99,11 @@ const BookingWidget = ({
|
|
|
98
99
|
const [checkoutShowPaymentForm, setCheckoutShowPaymentForm] = useState(false);
|
|
99
100
|
const [isClosing, setIsClosing] = useState(false);
|
|
100
101
|
const [isReadyForOpen, setIsReadyForOpen] = useState(false);
|
|
102
|
+
const [configLoading, setConfigLoading] = useState(false);
|
|
103
|
+
const [configLoaded, setConfigLoaded] = useState(false);
|
|
104
|
+
const [configError, setConfigError] = useState(null);
|
|
105
|
+
const [runtimeWidgetStyles, setRuntimeWidgetStyles] = useState({});
|
|
106
|
+
const [configRetryCount, setConfigRetryCount] = useState(0);
|
|
101
107
|
const widgetRef = useRef(null);
|
|
102
108
|
const stripeRef = useRef(null);
|
|
103
109
|
const elementsRef = useRef(null);
|
|
@@ -220,26 +226,43 @@ const BookingWidget = ({
|
|
|
220
226
|
};
|
|
221
227
|
}, [state.step, confirmationToken, effectiveConfirmationBaseUrl, isSandbox]);
|
|
222
228
|
|
|
223
|
-
//
|
|
229
|
+
// Fetch runtime config (colors) from /load-config on mount and when propertyKey/colors change.
|
|
230
|
+
// The config service caches API results so re-renders triggered by other prop changes are cheap.
|
|
224
231
|
useEffect(() => {
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (colors.text) {
|
|
232
|
-
style.setProperty('--fg', colors.text);
|
|
233
|
-
style.setProperty('--card-fg', colors.text);
|
|
234
|
-
}
|
|
235
|
-
if (colors.primary) {
|
|
236
|
-
style.setProperty('--primary', colors.primary);
|
|
237
|
-
}
|
|
238
|
-
if (colors.primaryText) {
|
|
239
|
-
style.setProperty('--primary-fg', colors.primaryText);
|
|
240
|
-
}
|
|
232
|
+
if (!propertyKey || !String(propertyKey).trim()) {
|
|
233
|
+
setConfigError('propertyKey is required to initialize the booking widget.');
|
|
234
|
+
setConfigLoading(false);
|
|
235
|
+
setConfigLoaded(false);
|
|
236
|
+
setRuntimeWidgetStyles({});
|
|
237
|
+
return;
|
|
241
238
|
}
|
|
242
|
-
|
|
239
|
+
let cancelled = false;
|
|
240
|
+
setConfigLoading(true);
|
|
241
|
+
setConfigError(null);
|
|
242
|
+
setConfigLoaded(false);
|
|
243
|
+
fetchRuntimeConfig(propertyKey, colors)
|
|
244
|
+
.then(({ widgetStyles }) => {
|
|
245
|
+
if (cancelled) return;
|
|
246
|
+
setRuntimeWidgetStyles(widgetStyles);
|
|
247
|
+
setConfigLoaded(true);
|
|
248
|
+
setConfigLoading(false);
|
|
249
|
+
})
|
|
250
|
+
.catch((err) => {
|
|
251
|
+
if (cancelled) return;
|
|
252
|
+
setConfigError(err?.message || 'Failed to load widget configuration.');
|
|
253
|
+
setConfigLoading(false);
|
|
254
|
+
});
|
|
255
|
+
return () => { cancelled = true; };
|
|
256
|
+
}, [propertyKey, colors, configRetryCount]);
|
|
257
|
+
|
|
258
|
+
// Apply resolved CSS custom properties to the widget element.
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
if (!widgetRef.current) return;
|
|
261
|
+
const style = widgetRef.current.style;
|
|
262
|
+
Object.entries(runtimeWidgetStyles).forEach(([key, val]) => {
|
|
263
|
+
if (val != null && val !== '') style.setProperty(key, val);
|
|
264
|
+
});
|
|
265
|
+
}, [runtimeWidgetStyles, isOpen]);
|
|
243
266
|
|
|
244
267
|
const getNights = () => {
|
|
245
268
|
if (!state.checkIn || !state.checkOut) return 0;
|
|
@@ -278,6 +301,8 @@ const BookingWidget = ({
|
|
|
278
301
|
const stepIndex = (key) => STEPS.findIndex(s => s.key === key);
|
|
279
302
|
|
|
280
303
|
const goToStep = (step) => {
|
|
304
|
+
// Block room/rate navigation until runtime config (colors) has been loaded.
|
|
305
|
+
if ((step === 'rooms' || step === 'rates') && !configLoaded) return;
|
|
281
306
|
setApiError(null);
|
|
282
307
|
if (step !== 'summary' && step !== 'payment') setCheckoutShowPaymentForm(false);
|
|
283
308
|
if (step === 'payment') setCheckoutShowPaymentForm(true);
|
|
@@ -1181,20 +1206,58 @@ const BookingWidget = ({
|
|
|
1181
1206
|
onTransitionEnd={handleTransitionEnd}
|
|
1182
1207
|
>
|
|
1183
1208
|
<button className="booking-widget-close" onClick={requestClose}><X size={24} /></button>
|
|
1184
|
-
{hasPropertyKey && renderStepIndicator()}
|
|
1209
|
+
{configLoaded && hasPropertyKey && renderStepIndicator()}
|
|
1185
1210
|
{(!propertyKey || (typeof propertyKey === 'string' && !propertyKey.trim())) ? (
|
|
1186
|
-
<div className="booking-widget-
|
|
1187
|
-
|
|
1211
|
+
<div className="booking-widget-config-error" role="alert">
|
|
1212
|
+
<div className="booking-widget-config-error__icon-wrap">
|
|
1213
|
+
<Lock size={22} strokeWidth={1.5} />
|
|
1214
|
+
</div>
|
|
1215
|
+
<span className="booking-widget-config-error__badge">
|
|
1216
|
+
<Lock size={10} strokeWidth={2} /> Missing Configuration
|
|
1217
|
+
</span>
|
|
1218
|
+
<h3 className="booking-widget-config-error__title">Widget Not Configured</h3>
|
|
1219
|
+
<p className="booking-widget-config-error__desc">
|
|
1220
|
+
A <code>propertyKey</code> prop is required to initialize this booking widget. Please provide it to load rooms and availability.
|
|
1221
|
+
</p>
|
|
1222
|
+
<div className="booking-widget-config-error__divider" />
|
|
1223
|
+
<p className="booking-widget-config-error__hint">
|
|
1224
|
+
Contact the site administrator to configure this widget.
|
|
1225
|
+
</p>
|
|
1226
|
+
</div>
|
|
1227
|
+
) : configError ? (
|
|
1228
|
+
<div className="booking-widget-config-error" role="alert">
|
|
1229
|
+
<div className="booking-widget-config-error__icon-wrap">
|
|
1230
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="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"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
|
1231
|
+
</div>
|
|
1232
|
+
<span className="booking-widget-config-error__badge">Configuration Error</span>
|
|
1233
|
+
<h3 className="booking-widget-config-error__title">Could Not Load Config</h3>
|
|
1234
|
+
<p className="booking-widget-config-error__desc">{configError}</p>
|
|
1235
|
+
<div className="booking-widget-config-error__divider" />
|
|
1236
|
+
<p className="booking-widget-config-error__hint">
|
|
1237
|
+
Please try again or contact support.
|
|
1238
|
+
</p>
|
|
1239
|
+
<button
|
|
1240
|
+
className="booking-widget-config-error__retry"
|
|
1241
|
+
onClick={() => setConfigRetryCount(c => c + 1)}
|
|
1242
|
+
>
|
|
1243
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><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"/></svg>
|
|
1244
|
+
Try Again
|
|
1245
|
+
</button>
|
|
1246
|
+
</div>
|
|
1247
|
+
) : configLoading ? (
|
|
1248
|
+
<div className="booking-widget-config-loading">
|
|
1249
|
+
<div className="booking-widget-config-loading__spinner" />
|
|
1250
|
+
<span className="booking-widget-config-loading__text">Loading configuration…</span>
|
|
1188
1251
|
</div>
|
|
1189
1252
|
) : (
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1253
|
+
<div className="booking-widget-step-content">
|
|
1254
|
+
{state.step === 'dates' && renderDatesStep()}
|
|
1255
|
+
{state.step === 'rooms' && renderRoomsStep()}
|
|
1256
|
+
{state.step === 'rates' && renderRatesStep()}
|
|
1257
|
+
{state.step === 'summary' && renderSummaryStep()}
|
|
1258
|
+
{state.step === 'payment' && renderPaymentStep()}
|
|
1259
|
+
{state.step === 'confirmation' && renderConfirmationStep()}
|
|
1260
|
+
</div>
|
|
1198
1261
|
)}
|
|
1199
1262
|
</div>
|
|
1200
1263
|
</>
|
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="hasPropertyKey && 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,9 +34,45 @@
|
|
|
34
34
|
</span>
|
|
35
35
|
</template>
|
|
36
36
|
</div>
|
|
37
|
-
|
|
38
|
-
|
|
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>
|
|
39
74
|
</div>
|
|
75
|
+
|
|
40
76
|
<div v-else class="booking-widget-step-content">
|
|
41
77
|
<!-- Dates Step -->
|
|
42
78
|
<div v-if="state.step === 'dates'">
|
|
@@ -445,6 +481,8 @@ import { loadStripe } from '@stripe/stripe-js';
|
|
|
445
481
|
import '../core/styles.css';
|
|
446
482
|
import { createBookingApi, formatPrice, buildCheckoutPayload, buildPaymentIntentPayload } from '../core/booking-api.js';
|
|
447
483
|
import { STRIPE_PUBLISHABLE_KEY, API_BASE_URL } from '../core/stripe-config.js';
|
|
484
|
+
import { fetchRuntimeConfig } from '../utils/config-service.js';
|
|
485
|
+
|
|
448
486
|
|
|
449
487
|
const BASE_STEPS = [
|
|
450
488
|
{ key: 'dates', label: 'Dates & Guests', num: '01' },
|
|
@@ -497,6 +535,10 @@ export default {
|
|
|
497
535
|
loadingRates: false,
|
|
498
536
|
apiError: null,
|
|
499
537
|
confirmationCode: null,
|
|
538
|
+
configLoading: false,
|
|
539
|
+
configLoaded: false,
|
|
540
|
+
configError: null,
|
|
541
|
+
runtimeWidgetStyles: {},
|
|
500
542
|
calendarMonth: new Date().getMonth(),
|
|
501
543
|
calendarYear: new Date().getFullYear(),
|
|
502
544
|
pickState: 0,
|
|
@@ -595,23 +637,7 @@ export default {
|
|
|
595
637
|
return null;
|
|
596
638
|
},
|
|
597
639
|
widgetStyles() {
|
|
598
|
-
|
|
599
|
-
const styles = {};
|
|
600
|
-
if (this.colors.background) {
|
|
601
|
-
styles['--bg'] = this.colors.background;
|
|
602
|
-
styles['--card'] = this.colors.background;
|
|
603
|
-
}
|
|
604
|
-
if (this.colors.text) {
|
|
605
|
-
styles['--fg'] = this.colors.text;
|
|
606
|
-
styles['--card-fg'] = this.colors.text;
|
|
607
|
-
}
|
|
608
|
-
if (this.colors.primary) {
|
|
609
|
-
styles['--primary'] = this.colors.primary;
|
|
610
|
-
}
|
|
611
|
-
if (this.colors.primaryText) {
|
|
612
|
-
styles['--primary-fg'] = this.colors.primaryText;
|
|
613
|
-
}
|
|
614
|
-
return styles;
|
|
640
|
+
return this.runtimeWidgetStyles;
|
|
615
641
|
},
|
|
616
642
|
nights() {
|
|
617
643
|
if (!this.state.checkIn || !this.state.checkOut) return 0;
|
|
@@ -697,7 +723,17 @@ export default {
|
|
|
697
723
|
return this.isOpen && !this.isClosing && this.isReadyForOpen;
|
|
698
724
|
},
|
|
699
725
|
},
|
|
726
|
+
created() {
|
|
727
|
+
this._initRuntimeConfig();
|
|
728
|
+
},
|
|
700
729
|
watch: {
|
|
730
|
+
propertyKey() {
|
|
731
|
+
this._initRuntimeConfig();
|
|
732
|
+
},
|
|
733
|
+
colors: {
|
|
734
|
+
deep: true,
|
|
735
|
+
handler() { this._initRuntimeConfig(); },
|
|
736
|
+
},
|
|
701
737
|
isOpen: {
|
|
702
738
|
handler(open) {
|
|
703
739
|
if (open && this.onOpen) this.onOpen();
|
|
@@ -731,6 +767,27 @@ export default {
|
|
|
731
767
|
},
|
|
732
768
|
},
|
|
733
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
|
+
},
|
|
734
791
|
async fetchConfirmationDetails(token) {
|
|
735
792
|
const t = String(token || '').trim();
|
|
736
793
|
if (!t) throw new Error('Missing confirmation token');
|
|
@@ -802,6 +859,8 @@ export default {
|
|
|
802
859
|
return i === ci ? 'active' : i < ci ? 'past' : 'future';
|
|
803
860
|
},
|
|
804
861
|
goToStep(step) {
|
|
862
|
+
// Block navigation to room/rate steps until runtime config is loaded.
|
|
863
|
+
if ((step === 'rooms' || step === 'rates') && !this.configLoaded) return;
|
|
805
864
|
if (step !== 'summary' && step !== 'payment') this.checkoutShowPaymentForm = false;
|
|
806
865
|
if (step === 'payment') this.checkoutShowPaymentForm = true;
|
|
807
866
|
this.state.step = step;
|