@lifestreamdynamics/booking-widget 0.1.0

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.
@@ -0,0 +1,784 @@
1
+ var v = Object.defineProperty;
2
+ var y = (p, u, e) => u in p ? v(p, u, { enumerable: !0, configurable: !0, writable: !0, value: e }) : p[u] = e;
3
+ var d = (p, u, e) => y(p, typeof u != "symbol" ? u + "" : u, e);
4
+ const k = `
5
+ :host {
6
+ display: block;
7
+ font-family: system-ui, -apple-system, sans-serif;
8
+ --lsv-primary-color: #06b6d4;
9
+ --lsv-border-radius: 0.5rem;
10
+ }
11
+
12
+ :host([theme="light"]) {
13
+ --lsv-bg-color: #ffffff;
14
+ --lsv-text-color: #1e293b;
15
+ --lsv-surface-color: #f8fafc;
16
+ --lsv-border-color: #e2e8f0;
17
+ --lsv-muted-color: #64748b;
18
+ --lsv-input-bg: #ffffff;
19
+ }
20
+
21
+ :host([theme="dark"]), :host {
22
+ --lsv-bg-color: #0f172a;
23
+ --lsv-text-color: #e2e8f0;
24
+ --lsv-surface-color: #1e293b;
25
+ --lsv-border-color: #334155;
26
+ --lsv-muted-color: #94a3b8;
27
+ --lsv-input-bg: #0f172a;
28
+ }
29
+
30
+ @media (prefers-color-scheme: light) {
31
+ :host([theme="auto"]) {
32
+ --lsv-bg-color: #ffffff;
33
+ --lsv-text-color: #1e293b;
34
+ --lsv-surface-color: #f8fafc;
35
+ --lsv-border-color: #e2e8f0;
36
+ --lsv-muted-color: #64748b;
37
+ --lsv-input-bg: #ffffff;
38
+ }
39
+ }
40
+
41
+ @media (prefers-color-scheme: dark) {
42
+ :host([theme="auto"]) {
43
+ --lsv-bg-color: #0f172a;
44
+ --lsv-text-color: #e2e8f0;
45
+ --lsv-surface-color: #1e293b;
46
+ --lsv-border-color: #334155;
47
+ --lsv-muted-color: #94a3b8;
48
+ --lsv-input-bg: #0f172a;
49
+ }
50
+ }
51
+
52
+ * { box-sizing: border-box; }
53
+
54
+ .widget {
55
+ background: var(--lsv-bg-color);
56
+ color: var(--lsv-text-color);
57
+ border: 1px solid var(--lsv-border-color);
58
+ border-radius: var(--lsv-border-radius);
59
+ padding: 1.5rem;
60
+ max-width: 480px;
61
+ margin: 0 auto;
62
+ }
63
+
64
+ .widget-title {
65
+ font-size: 1.125rem;
66
+ font-weight: 600;
67
+ margin: 0 0 1rem;
68
+ }
69
+
70
+ .step-indicator {
71
+ display: flex;
72
+ gap: 0.5rem;
73
+ margin-bottom: 1.25rem;
74
+ }
75
+
76
+ .step-dot {
77
+ width: 8px;
78
+ height: 8px;
79
+ border-radius: 50%;
80
+ background: var(--lsv-border-color);
81
+ transition: background 0.2s;
82
+ }
83
+
84
+ .step-dot.active {
85
+ background: var(--lsv-primary-color);
86
+ }
87
+
88
+ .step-dot.done {
89
+ background: var(--lsv-primary-color);
90
+ opacity: 0.5;
91
+ }
92
+
93
+ /* Loading */
94
+ .loading {
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ gap: 0.5rem;
99
+ padding: 2rem 0;
100
+ color: var(--lsv-muted-color);
101
+ font-size: 0.875rem;
102
+ }
103
+
104
+ .spinner {
105
+ width: 20px;
106
+ height: 20px;
107
+ border: 2px solid var(--lsv-border-color);
108
+ border-top-color: var(--lsv-primary-color);
109
+ border-radius: 50%;
110
+ animation: spin 0.8s linear infinite;
111
+ }
112
+
113
+ @keyframes spin {
114
+ to { transform: rotate(360deg); }
115
+ }
116
+
117
+ /* Error */
118
+ .error-box {
119
+ background: rgba(239, 68, 68, 0.1);
120
+ border: 1px solid rgba(239, 68, 68, 0.4);
121
+ border-radius: var(--lsv-border-radius);
122
+ padding: 0.875rem 1rem;
123
+ font-size: 0.875rem;
124
+ color: #f87171;
125
+ margin-bottom: 0.75rem;
126
+ }
127
+
128
+ .btn-retry {
129
+ display: inline-block;
130
+ margin-top: 0.75rem;
131
+ padding: 0.5rem 1rem;
132
+ background: var(--lsv-surface-color);
133
+ border: 1px solid var(--lsv-border-color);
134
+ border-radius: var(--lsv-border-radius);
135
+ color: var(--lsv-text-color);
136
+ font-size: 0.8125rem;
137
+ cursor: pointer;
138
+ }
139
+
140
+ .btn-retry:hover { opacity: 0.8; }
141
+
142
+ /* Slot cards */
143
+ .slot-list {
144
+ display: flex;
145
+ flex-direction: column;
146
+ gap: 0.625rem;
147
+ }
148
+
149
+ .slot-card {
150
+ background: var(--lsv-surface-color);
151
+ border: 1px solid var(--lsv-border-color);
152
+ border-radius: var(--lsv-border-radius);
153
+ padding: 0.875rem 1rem;
154
+ cursor: pointer;
155
+ transition: border-color 0.15s;
156
+ text-align: left;
157
+ width: 100%;
158
+ color: var(--lsv-text-color);
159
+ }
160
+
161
+ .slot-card:hover {
162
+ border-color: var(--lsv-primary-color);
163
+ }
164
+
165
+ .slot-card-title {
166
+ font-weight: 500;
167
+ font-size: 0.9375rem;
168
+ margin: 0 0 0.25rem;
169
+ }
170
+
171
+ .slot-card-meta {
172
+ font-size: 0.8125rem;
173
+ color: var(--lsv-muted-color);
174
+ margin: 0;
175
+ }
176
+
177
+ /* Date picker */
178
+ .date-grid {
179
+ display: grid;
180
+ grid-template-columns: repeat(7, 1fr);
181
+ gap: 0.25rem;
182
+ margin-bottom: 0.75rem;
183
+ }
184
+
185
+ .date-header {
186
+ font-size: 0.6875rem;
187
+ font-weight: 600;
188
+ text-align: center;
189
+ color: var(--lsv-muted-color);
190
+ padding: 0.25rem 0;
191
+ }
192
+
193
+ .date-cell {
194
+ aspect-ratio: 1;
195
+ display: flex;
196
+ align-items: center;
197
+ justify-content: center;
198
+ border-radius: calc(var(--lsv-border-radius) * 0.5);
199
+ font-size: 0.8125rem;
200
+ cursor: pointer;
201
+ border: 1px solid transparent;
202
+ background: transparent;
203
+ color: var(--lsv-text-color);
204
+ transition: background 0.15s, border-color 0.15s;
205
+ }
206
+
207
+ .date-cell:hover:not(.disabled) {
208
+ background: var(--lsv-surface-color);
209
+ border-color: var(--lsv-border-color);
210
+ }
211
+
212
+ .date-cell.selected {
213
+ background: var(--lsv-primary-color);
214
+ color: #0f172a;
215
+ font-weight: 600;
216
+ }
217
+
218
+ .date-cell.disabled {
219
+ opacity: 0.3;
220
+ cursor: not-allowed;
221
+ }
222
+
223
+ .date-cell.empty {
224
+ cursor: default;
225
+ }
226
+
227
+ /* Time slots */
228
+ .time-grid {
229
+ display: grid;
230
+ grid-template-columns: repeat(3, 1fr);
231
+ gap: 0.5rem;
232
+ }
233
+
234
+ .time-btn {
235
+ padding: 0.5rem 0.25rem;
236
+ border: 1px solid var(--lsv-border-color);
237
+ border-radius: var(--lsv-border-radius);
238
+ background: var(--lsv-surface-color);
239
+ color: var(--lsv-text-color);
240
+ font-size: 0.8125rem;
241
+ cursor: pointer;
242
+ transition: border-color 0.15s, background 0.15s;
243
+ }
244
+
245
+ .time-btn:hover {
246
+ border-color: var(--lsv-primary-color);
247
+ }
248
+
249
+ .time-btn.selected {
250
+ background: var(--lsv-primary-color);
251
+ border-color: var(--lsv-primary-color);
252
+ color: #0f172a;
253
+ font-weight: 600;
254
+ }
255
+
256
+ /* Form */
257
+ .form-group {
258
+ margin-bottom: 0.875rem;
259
+ }
260
+
261
+ .form-label {
262
+ display: block;
263
+ font-size: 0.8125rem;
264
+ font-weight: 500;
265
+ margin-bottom: 0.375rem;
266
+ color: var(--lsv-text-color);
267
+ }
268
+
269
+ .form-input, .form-textarea {
270
+ width: 100%;
271
+ padding: 0.5rem 0.75rem;
272
+ background: var(--lsv-input-bg);
273
+ border: 1px solid var(--lsv-border-color);
274
+ border-radius: var(--lsv-border-radius);
275
+ color: var(--lsv-text-color);
276
+ font-size: 0.875rem;
277
+ font-family: inherit;
278
+ transition: border-color 0.15s;
279
+ }
280
+
281
+ .form-input:focus, .form-textarea:focus {
282
+ outline: none;
283
+ border-color: var(--lsv-primary-color);
284
+ }
285
+
286
+ .form-textarea {
287
+ resize: vertical;
288
+ min-height: 80px;
289
+ }
290
+
291
+ /* Buttons */
292
+ .btn-primary {
293
+ display: block;
294
+ width: 100%;
295
+ padding: 0.625rem 1rem;
296
+ background: var(--lsv-primary-color);
297
+ border: none;
298
+ border-radius: var(--lsv-border-radius);
299
+ color: #0f172a;
300
+ font-size: 0.9375rem;
301
+ font-weight: 600;
302
+ cursor: pointer;
303
+ transition: opacity 0.15s;
304
+ font-family: inherit;
305
+ }
306
+
307
+ .btn-primary:hover:not(:disabled) { opacity: 0.85; }
308
+ .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
309
+
310
+ .btn-back {
311
+ display: inline-flex;
312
+ align-items: center;
313
+ gap: 0.25rem;
314
+ padding: 0.375rem 0;
315
+ background: transparent;
316
+ border: none;
317
+ color: var(--lsv-muted-color);
318
+ font-size: 0.8125rem;
319
+ cursor: pointer;
320
+ margin-bottom: 0.75rem;
321
+ font-family: inherit;
322
+ }
323
+
324
+ .btn-back:hover { color: var(--lsv-text-color); }
325
+
326
+ /* Summary */
327
+ .booking-summary {
328
+ background: var(--lsv-surface-color);
329
+ border: 1px solid var(--lsv-border-color);
330
+ border-radius: var(--lsv-border-radius);
331
+ padding: 0.75rem 1rem;
332
+ margin-bottom: 1rem;
333
+ font-size: 0.8125rem;
334
+ color: var(--lsv-muted-color);
335
+ }
336
+
337
+ .booking-summary strong {
338
+ color: var(--lsv-text-color);
339
+ display: block;
340
+ font-size: 0.9375rem;
341
+ margin-bottom: 0.25rem;
342
+ }
343
+
344
+ /* Success */
345
+ .success-view {
346
+ text-align: center;
347
+ padding: 1.5rem 0;
348
+ }
349
+
350
+ .success-icon {
351
+ width: 48px;
352
+ height: 48px;
353
+ border-radius: 50%;
354
+ background: rgba(6, 182, 212, 0.15);
355
+ display: flex;
356
+ align-items: center;
357
+ justify-content: center;
358
+ margin: 0 auto 1rem;
359
+ font-size: 1.5rem;
360
+ }
361
+
362
+ .success-title {
363
+ font-size: 1.125rem;
364
+ font-weight: 600;
365
+ margin: 0 0 0.5rem;
366
+ }
367
+
368
+ .success-msg {
369
+ font-size: 0.875rem;
370
+ color: var(--lsv-muted-color);
371
+ margin: 0;
372
+ }
373
+
374
+ .section-title {
375
+ font-size: 0.875rem;
376
+ font-weight: 600;
377
+ color: var(--lsv-muted-color);
378
+ margin: 0 0 0.75rem;
379
+ text-transform: uppercase;
380
+ letter-spacing: 0.05em;
381
+ }
382
+
383
+ .empty-msg {
384
+ padding: 1.5rem 0;
385
+ text-align: center;
386
+ color: var(--lsv-muted-color);
387
+ font-size: 0.875rem;
388
+ }
389
+
390
+ .actions {
391
+ display: flex;
392
+ flex-direction: column;
393
+ gap: 0.5rem;
394
+ margin-top: 1rem;
395
+ }
396
+ `, C = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"], x = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
397
+ class f extends HTMLElement {
398
+ constructor() {
399
+ super();
400
+ d(this, "root");
401
+ d(this, "step", "slots");
402
+ d(this, "slots", []);
403
+ d(this, "selectedSlot", null);
404
+ d(this, "selectedDate", "");
405
+ d(this, "selectedTime", "");
406
+ d(this, "availableTimes", []);
407
+ d(this, "isLoading", !1);
408
+ d(this, "errorMsg", "");
409
+ this.root = this.attachShadow({ mode: "open" });
410
+ }
411
+ connectedCallback() {
412
+ this.render(), this.loadSlots();
413
+ }
414
+ attributeChangedCallback(e, t, r) {
415
+ t !== r && (e === "theme" ? this.render() : this.isConnected && this.reset());
416
+ }
417
+ // ── Getters ──────────────────────────────────────────────────────────────
418
+ get apiUrl() {
419
+ return (this.getAttribute("api-url") ?? "").replace(/\/$/, "");
420
+ }
421
+ get profileSlug() {
422
+ return this.getAttribute("profile-slug") ?? "";
423
+ }
424
+ get vaultSlug() {
425
+ return this.getAttribute("vault-slug") ?? "";
426
+ }
427
+ get baseApiPath() {
428
+ return `${this.apiUrl}/api/v1/public/vaults/${this.profileSlug}/${this.vaultSlug}`;
429
+ }
430
+ // ── State helpers ────────────────────────────────────────────────────────
431
+ reset() {
432
+ this.step = "slots", this.slots = [], this.selectedSlot = null, this.selectedDate = "", this.selectedTime = "", this.availableTimes = [], this.isLoading = !1, this.errorMsg = "", this.render(), this.loadSlots();
433
+ }
434
+ setLoading(e) {
435
+ this.isLoading = e, this.render();
436
+ }
437
+ setError(e) {
438
+ this.errorMsg = e, this.isLoading = !1, this.render();
439
+ }
440
+ // ── API calls ─────────────────────────────────────────────────────────────
441
+ async loadSlots() {
442
+ this.setLoading(!0);
443
+ try {
444
+ const e = await fetch(`${this.baseApiPath}/booking-slots`);
445
+ if (!e.ok) throw new Error(`HTTP ${e.status}`);
446
+ const t = await e.json();
447
+ this.slots = t.slots, this.errorMsg = "";
448
+ } catch {
449
+ this.setError("Failed to load booking slots. Please try again.");
450
+ return;
451
+ }
452
+ this.isLoading = !1, this.render();
453
+ }
454
+ async loadTimes(e, t) {
455
+ this.setLoading(!0);
456
+ try {
457
+ const r = await fetch(
458
+ `${this.baseApiPath}/booking-slots/${e}/availability?date=${t}`
459
+ );
460
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
461
+ const o = await r.json();
462
+ this.availableTimes = o.times, this.errorMsg = "";
463
+ } catch {
464
+ this.setError("Failed to load available times. Please try again.");
465
+ return;
466
+ }
467
+ this.isLoading = !1, this.step = "time", this.render();
468
+ }
469
+ async submitBooking(e, t, r, o) {
470
+ if (!this.selectedSlot || !this.selectedDate || !this.selectedTime) return;
471
+ this.setLoading(!0);
472
+ const s = (/* @__PURE__ */ new Date(`${this.selectedDate}T${this.selectedTime}`)).toISOString(), a = this.selectedSlot.id, n = {
473
+ guestName: e,
474
+ guestEmail: t,
475
+ startAt: s
476
+ };
477
+ r && (n.guestPhone = r), o && (n.notes = o);
478
+ try {
479
+ const c = await fetch(
480
+ `${this.baseApiPath}/booking-slots/${a}/book`,
481
+ {
482
+ method: "POST",
483
+ headers: { "Content-Type": "application/json" },
484
+ body: JSON.stringify(n)
485
+ }
486
+ );
487
+ if (!c.ok) {
488
+ const l = await c.json().catch(() => ({}));
489
+ throw new Error(l.error ?? l.message ?? `HTTP ${c.status}`);
490
+ }
491
+ const i = await c.json();
492
+ this.isLoading = !1, this.step = "success", this.render(), this.dispatchEvent(new CustomEvent("lsv-booking-submitted", {
493
+ bubbles: !0,
494
+ composed: !0,
495
+ detail: {
496
+ bookingId: "",
497
+ startAt: i.startAt,
498
+ endAt: "",
499
+ slotTitle: this.selectedSlot.title
500
+ }
501
+ }));
502
+ } catch (c) {
503
+ const i = c instanceof Error ? c.message : "Booking failed. Please try again.";
504
+ this.dispatchEvent(new CustomEvent("lsv-booking-error", {
505
+ bubbles: !0,
506
+ composed: !0,
507
+ detail: { message: i, step: "form" }
508
+ })), this.setError(i);
509
+ }
510
+ }
511
+ // ── Date helpers ──────────────────────────────────────────────────────────
512
+ getNextDates() {
513
+ const e = [], t = /* @__PURE__ */ new Date();
514
+ t.setHours(0, 0, 0, 0);
515
+ for (let r = 0; r < 14; r++) {
516
+ const o = new Date(t);
517
+ o.setDate(t.getDate() + r);
518
+ const s = o.toISOString().split("T")[0], a = o.toLocaleDateString("en-US", { month: "short", day: "numeric" });
519
+ e.push({ date: s, label: a, dayOfWeek: o.getDay() });
520
+ }
521
+ return e;
522
+ }
523
+ isDateAllowed(e) {
524
+ if (!this.selectedSlot) return !1;
525
+ const t = C[e];
526
+ return this.selectedSlot.daysOfWeek.includes(t);
527
+ }
528
+ formatTime(e) {
529
+ try {
530
+ if (e.includes("T"))
531
+ return new Date(e).toLocaleTimeString("en-US", {
532
+ hour: "numeric",
533
+ minute: "2-digit",
534
+ hour12: !0
535
+ });
536
+ const [t, r] = e.split(":").map(Number), o = /* @__PURE__ */ new Date();
537
+ return o.setHours(t, r, 0, 0), o.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: !0 });
538
+ } catch {
539
+ return e;
540
+ }
541
+ }
542
+ formatDate(e) {
543
+ try {
544
+ return (/* @__PURE__ */ new Date(`${e}T12:00:00`)).toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
545
+ } catch {
546
+ return e;
547
+ }
548
+ }
549
+ // ── Render ────────────────────────────────────────────────────────────────
550
+ render() {
551
+ const e = document.createElement("style");
552
+ e.textContent = k;
553
+ const t = document.createElement("div");
554
+ if (t.className = "widget", this.step === "success")
555
+ t.appendChild(this.renderSuccess());
556
+ else if (t.appendChild(this.renderStepIndicator()), this.isLoading)
557
+ t.appendChild(this.renderLoading());
558
+ else if (this.errorMsg)
559
+ t.appendChild(this.renderError());
560
+ else
561
+ switch (this.step) {
562
+ case "slots":
563
+ t.appendChild(this.renderSlots());
564
+ break;
565
+ case "date":
566
+ t.appendChild(this.renderDate());
567
+ break;
568
+ case "time":
569
+ t.appendChild(this.renderTime());
570
+ break;
571
+ case "form":
572
+ t.appendChild(this.renderForm());
573
+ break;
574
+ }
575
+ this.root.replaceChildren(e, t), this.attachEvents();
576
+ }
577
+ renderStepIndicator() {
578
+ const e = ["slots", "date", "time", "form"], t = e.indexOf(this.step), r = document.createElement("div");
579
+ return r.className = "step-indicator", e.forEach((o, s) => {
580
+ const a = document.createElement("div");
581
+ a.className = `step-dot${s === t ? " active" : s < t ? " done" : ""}`, r.appendChild(a);
582
+ }), r;
583
+ }
584
+ renderLoading() {
585
+ const e = document.createElement("div");
586
+ e.className = "loading";
587
+ const t = document.createElement("div");
588
+ t.className = "spinner";
589
+ const r = document.createElement("span");
590
+ return r.textContent = "Loading…", e.appendChild(t), e.appendChild(r), e;
591
+ }
592
+ renderError() {
593
+ const e = document.createDocumentFragment(), t = document.createElement("div");
594
+ t.className = "error-box", t.textContent = this.errorMsg, e.appendChild(t);
595
+ const r = document.createElement("button");
596
+ r.className = "btn-retry", r.dataset.action = "retry", r.textContent = "Try again", e.appendChild(r);
597
+ const o = document.createElement("div");
598
+ return o.appendChild(e), o;
599
+ }
600
+ renderSlots() {
601
+ const e = document.createDocumentFragment(), t = document.createElement("p");
602
+ if (t.className = "section-title", t.textContent = "Choose a booking type", e.appendChild(t), this.slots.length === 0) {
603
+ const o = document.createElement("p");
604
+ o.className = "empty-msg", o.textContent = "No booking slots available.", e.appendChild(o);
605
+ } else {
606
+ const o = document.createElement("div");
607
+ o.className = "slot-list", this.slots.forEach((s) => {
608
+ const a = document.createElement("button");
609
+ a.className = "slot-card", a.dataset.action = "select-slot", a.dataset.slotId = s.id;
610
+ const n = document.createElement("p");
611
+ n.className = "slot-card-title", n.textContent = s.title;
612
+ const c = document.createElement("p");
613
+ c.className = "slot-card-meta";
614
+ const i = this.formatDayList(s.daysOfWeek);
615
+ if (c.textContent = `${s.durationMin} min · ${i} · ${s.startTime}–${s.endTime}`, a.appendChild(n), s.description) {
616
+ const l = document.createElement("p");
617
+ l.className = "slot-card-meta", l.style.marginTop = "0.25rem", l.textContent = s.description, a.appendChild(l);
618
+ }
619
+ a.appendChild(c), o.appendChild(a);
620
+ }), e.appendChild(o);
621
+ }
622
+ const r = document.createElement("div");
623
+ return r.appendChild(e), r;
624
+ }
625
+ formatDayList(e) {
626
+ if (e.length === 7) return "Every day";
627
+ if (e.length === 5 && !e.includes("sat") && !e.includes("sun")) return "Weekdays";
628
+ if (e.length === 2 && e.includes("sat") && e.includes("sun")) return "Weekends";
629
+ const t = {
630
+ mon: "Mon",
631
+ tue: "Tue",
632
+ wed: "Wed",
633
+ thu: "Thu",
634
+ fri: "Fri",
635
+ sat: "Sat",
636
+ sun: "Sun"
637
+ };
638
+ return e.map((r) => t[r] ?? r).join(", ");
639
+ }
640
+ renderDate() {
641
+ var c;
642
+ const e = document.createDocumentFragment(), t = document.createElement("button");
643
+ t.className = "btn-back", t.dataset.action = "back-to-slots", t.textContent = "← Back", e.appendChild(t);
644
+ const r = document.createElement("p");
645
+ r.className = "section-title", r.textContent = "Select a date", e.appendChild(r);
646
+ const o = this.getNextDates(), s = document.createElement("div");
647
+ s.className = "date-grid", x.forEach((i) => {
648
+ const l = document.createElement("div");
649
+ l.className = "date-header", l.textContent = i, s.appendChild(l);
650
+ });
651
+ const a = ((c = o[0]) == null ? void 0 : c.dayOfWeek) ?? 0;
652
+ for (let i = 0; i < a; i++) {
653
+ const l = document.createElement("div");
654
+ l.className = "date-cell empty", s.appendChild(l);
655
+ }
656
+ o.forEach(({ date: i, label: l, dayOfWeek: h }) => {
657
+ const m = document.createElement("button"), g = this.isDateAllowed(h);
658
+ m.className = `date-cell${g ? "" : " disabled"}${i === this.selectedDate ? " selected" : ""}`, m.textContent = (/* @__PURE__ */ new Date(`${i}T12:00:00`)).getDate().toString(), m.title = l, g ? (m.dataset.action = "select-date", m.dataset.date = i) : m.disabled = !0, s.appendChild(m);
659
+ }), e.appendChild(s);
660
+ const n = document.createElement("div");
661
+ return n.appendChild(e), n;
662
+ }
663
+ renderTime() {
664
+ const e = document.createDocumentFragment(), t = document.createElement("button");
665
+ t.className = "btn-back", t.dataset.action = "back-to-date", t.textContent = "← Back", e.appendChild(t);
666
+ const r = document.createElement("p");
667
+ if (r.className = "section-title", r.textContent = `Available times · ${this.formatDate(this.selectedDate)}`, e.appendChild(r), this.availableTimes.length === 0) {
668
+ const s = document.createElement("p");
669
+ s.className = "empty-msg", s.textContent = "No times available on this date. Try another day.", e.appendChild(s);
670
+ } else {
671
+ const s = document.createElement("div");
672
+ s.className = "time-grid", this.availableTimes.forEach((a) => {
673
+ const n = document.createElement("button");
674
+ n.className = `time-btn${a === this.selectedTime ? " selected" : ""}`, n.dataset.action = "select-time", n.dataset.time = a, n.textContent = this.formatTime(a), s.appendChild(n);
675
+ }), e.appendChild(s);
676
+ }
677
+ if (this.selectedTime) {
678
+ const s = document.createElement("div");
679
+ s.className = "actions";
680
+ const a = document.createElement("button");
681
+ a.className = "btn-primary", a.dataset.action = "go-to-form", a.textContent = "Continue", s.appendChild(a), e.appendChild(s);
682
+ }
683
+ const o = document.createElement("div");
684
+ return o.appendChild(e), o;
685
+ }
686
+ renderForm() {
687
+ var g, b;
688
+ const e = document.createDocumentFragment(), t = document.createElement("button");
689
+ t.className = "btn-back", t.dataset.action = "back-to-time", t.textContent = "← Back", e.appendChild(t);
690
+ const r = document.createElement("div");
691
+ r.className = "booking-summary";
692
+ const o = document.createElement("strong");
693
+ o.textContent = ((g = this.selectedSlot) == null ? void 0 : g.title) ?? "", r.appendChild(o);
694
+ const s = document.createElement("span");
695
+ s.textContent = `${this.formatDate(this.selectedDate)} at ${this.formatTime(this.selectedTime)}`, r.appendChild(s), e.appendChild(r);
696
+ const a = document.createElement("form");
697
+ a.id = "lsv-booking-form", a.appendChild(this.makeField("guestName", "Full name", "text", !0)), a.appendChild(this.makeField("guestEmail", "Email address", "email", !0)), (b = this.selectedSlot) != null && b.requirePhone && a.appendChild(this.makeField("guestPhone", "Phone number", "tel", !0));
698
+ const n = document.createElement("div");
699
+ n.className = "form-group";
700
+ const c = document.createElement("label");
701
+ c.className = "form-label", c.setAttribute("for", "lsv-notes"), c.textContent = "Notes (optional)";
702
+ const i = document.createElement("textarea");
703
+ i.className = "form-textarea", i.id = "lsv-notes", i.name = "notes", i.rows = 3, i.placeholder = "Anything you'd like to share…", n.appendChild(c), n.appendChild(i), a.appendChild(n);
704
+ const l = document.createElement("div");
705
+ l.className = "actions";
706
+ const h = document.createElement("button");
707
+ h.type = "submit", h.className = "btn-primary", h.textContent = "Confirm Booking", l.appendChild(h), a.appendChild(l), e.appendChild(a);
708
+ const m = document.createElement("div");
709
+ return m.appendChild(e), m;
710
+ }
711
+ makeField(e, t, r, o) {
712
+ const s = document.createElement("div");
713
+ s.className = "form-group";
714
+ const a = document.createElement("label");
715
+ a.className = "form-label", a.setAttribute("for", `lsv-${e}`), a.textContent = o ? `${t} *` : t;
716
+ const n = document.createElement("input");
717
+ return n.className = "form-input", n.type = r, n.id = `lsv-${e}`, n.name = e, n.required = o, n.autocomplete = e === "guestName" ? "name" : e === "guestEmail" ? "email" : e === "guestPhone" ? "tel" : "off", s.appendChild(a), s.appendChild(n), s;
718
+ }
719
+ renderSuccess() {
720
+ const e = document.createElement("div");
721
+ e.className = "success-view";
722
+ const t = document.createElement("div");
723
+ t.className = "success-icon", t.textContent = "✓";
724
+ const r = document.createElement("p");
725
+ r.className = "success-title", r.textContent = "Booking requested!";
726
+ const o = document.createElement("p");
727
+ return o.className = "success-msg", o.textContent = "Check your email to confirm your booking.", e.appendChild(t), e.appendChild(r), e.appendChild(o), e;
728
+ }
729
+ // ── Event handling ────────────────────────────────────────────────────────
730
+ attachEvents() {
731
+ this.root.addEventListener("click", (t) => {
732
+ const o = t.target.closest("[data-action]");
733
+ o && this.handleAction(o.dataset.action ?? "", o);
734
+ });
735
+ const e = this.root.querySelector("#lsv-booking-form");
736
+ e && e.addEventListener("submit", (t) => {
737
+ t.preventDefault();
738
+ const r = new FormData(e), o = (r.get("guestName") ?? "").trim(), s = (r.get("guestEmail") ?? "").trim(), a = (r.get("guestPhone") ?? "").trim(), n = (r.get("notes") ?? "").trim();
739
+ !o || !s || this.submitBooking(o, s, a, n);
740
+ });
741
+ }
742
+ handleAction(e, t) {
743
+ switch (e) {
744
+ case "retry":
745
+ this.errorMsg = "", this.step === "slots" ? this.loadSlots() : this.step === "time" && this.selectedSlot ? this.loadTimes(this.selectedSlot.id, this.selectedDate) : this.render();
746
+ break;
747
+ case "select-slot": {
748
+ const r = t.dataset.slotId, o = this.slots.find((s) => s.id === r);
749
+ if (!o) return;
750
+ this.selectedSlot = o, this.step = "date", this.render();
751
+ break;
752
+ }
753
+ case "back-to-slots":
754
+ this.selectedSlot = null, this.selectedDate = "", this.selectedTime = "", this.step = "slots", this.render();
755
+ break;
756
+ case "select-date": {
757
+ const r = t.dataset.date;
758
+ if (!r || !this.selectedSlot) return;
759
+ this.selectedDate = r, this.selectedTime = "", this.loadTimes(this.selectedSlot.id, r);
760
+ break;
761
+ }
762
+ case "back-to-date":
763
+ this.selectedTime = "", this.step = "date", this.render();
764
+ break;
765
+ case "select-time": {
766
+ const r = t.dataset.time;
767
+ if (!r) return;
768
+ this.selectedTime = r, this.render();
769
+ break;
770
+ }
771
+ case "go-to-form":
772
+ this.selectedTime && (this.step = "form", this.render());
773
+ break;
774
+ case "back-to-time":
775
+ this.step = "time", this.render();
776
+ break;
777
+ }
778
+ }
779
+ }
780
+ d(f, "observedAttributes", ["api-url", "profile-slug", "vault-slug", "theme"]);
781
+ customElements.get("lsv-booking") || customElements.define("lsv-booking", f);
782
+ export {
783
+ f as LsvBooking
784
+ };