@springbrand/gravel 0.1.3-alpha.1 → 0.1.3-alpha.2
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/dist/_virtual/_commonjsHelpers.js +8 -0
- package/dist/_virtual/timezone.js +4 -0
- package/dist/_virtual/utc.js +4 -0
- package/dist/components/runtime-site/action/index.js +57 -0
- package/dist/components/runtime-site/floating-booking-bubble/api/booking-public.js +147 -0
- package/dist/components/runtime-site/floating-booking-bubble/booking-form-field.js +102 -0
- package/dist/components/runtime-site/floating-booking-bubble/bubble-container.js +89 -0
- package/dist/components/runtime-site/floating-booking-bubble/bubble-trigger.js +41 -0
- package/dist/components/runtime-site/floating-booking-bubble/dayjs-tz.js +9 -0
- package/dist/components/runtime-site/floating-booking-bubble/index.js +146 -0
- package/dist/components/runtime-site/floating-booking-bubble/public-appointment-model.js +92 -0
- package/dist/components/runtime-site/floating-booking-bubble/public-availability-model.js +14 -0
- package/dist/components/runtime-site/floating-booking-bubble/public-service-detail-model.js +33 -0
- package/dist/components/runtime-site/floating-booking-bubble/public-service-model.js +19 -0
- package/dist/components/runtime-site/floating-booking-bubble/step-1-service-picker.js +83 -0
- package/dist/components/runtime-site/floating-booking-bubble/step-2-time-picker.js +151 -0
- package/dist/components/runtime-site/floating-booking-bubble/step-3-details-form.js +142 -0
- package/dist/components/runtime-site/floating-booking-bubble/step-4-confirm.js +122 -0
- package/dist/components/runtime-site/floating-booking-bubble/use-booking-flow.js +287 -0
- package/dist/components/runtime-site/runtime-host-provider/index.js +20 -0
- package/dist/index.js +9 -1
- package/dist/node_modules/.pnpm/dayjs@1.11.20/node_modules/dayjs/plugin/timezone.js +67 -0
- package/dist/node_modules/.pnpm/dayjs@1.11.20/node_modules/dayjs/plugin/utc.js +79 -0
- package/package.json +12 -4
- package/dist/components/ui-base/index.d.ts +0 -1
- package/dist/components/ui-base/responsive-dialog/index.d.ts +0 -55
- package/dist/components/ui-block/background/index.d.ts +0 -8
- package/dist/components/ui-block/cover/index.d.ts +0 -17
- package/dist/components/ui-block/index.d.ts +0 -4
- package/dist/components/ui-block/sparkles/index.d.ts +0 -13
- package/dist/components/ui-block/spotlight/index.d.ts +0 -13
- package/dist/index.d.ts +0 -2
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
|
|
2
|
+
function getDefaultExportFromCjs(x) {
|
|
3
|
+
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
|
|
4
|
+
}
|
|
5
|
+
export {
|
|
6
|
+
commonjsGlobal,
|
|
7
|
+
getDefaultExportFromCjs
|
|
8
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Children, cloneElement } from "react";
|
|
3
|
+
import { useRuntimeHost } from "../runtime-host-provider/index.js";
|
|
4
|
+
function createRuntimeActionContext(trigger, event) {
|
|
5
|
+
const element = event.currentTarget;
|
|
6
|
+
const ownerDocument = element.ownerDocument;
|
|
7
|
+
return {
|
|
8
|
+
trigger,
|
|
9
|
+
event,
|
|
10
|
+
element,
|
|
11
|
+
ownerDocument,
|
|
12
|
+
ownerWindow: ownerDocument.defaultView
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function Action({
|
|
16
|
+
children,
|
|
17
|
+
id,
|
|
18
|
+
input,
|
|
19
|
+
trigger = "click"
|
|
20
|
+
}) {
|
|
21
|
+
const runtimeHost = useRuntimeHost();
|
|
22
|
+
const child = Children.only(children);
|
|
23
|
+
const originalHandler = trigger === "click" ? child.props.onClick : trigger === "pointerDown" ? child.props.onPointerDown : child.props.onSubmit;
|
|
24
|
+
const actionHandler = (event) => {
|
|
25
|
+
var _a, _b;
|
|
26
|
+
originalHandler == null ? void 0 : originalHandler(event);
|
|
27
|
+
if (event.defaultPrevented || !id || !runtimeHost.actions) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const context = createRuntimeActionContext(trigger, event);
|
|
31
|
+
try {
|
|
32
|
+
const result = runtimeHost.actions.runAction(id, input, context);
|
|
33
|
+
void Promise.resolve(result).catch((error) => {
|
|
34
|
+
var _a2, _b2;
|
|
35
|
+
(_b2 = (_a2 = runtimeHost.actions) == null ? void 0 : _a2.onError) == null ? void 0 : _b2.call(_a2, error, context);
|
|
36
|
+
});
|
|
37
|
+
} catch (error) {
|
|
38
|
+
(_b = (_a = runtimeHost.actions).onError) == null ? void 0 : _b.call(_a, error, context);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
if (trigger === "pointerDown") {
|
|
42
|
+
return cloneElement(child, {
|
|
43
|
+
onPointerDown: actionHandler
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (trigger === "submit") {
|
|
47
|
+
return cloneElement(child, {
|
|
48
|
+
onSubmit: actionHandler
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return cloneElement(child, {
|
|
52
|
+
onClick: actionHandler
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
Action
|
|
57
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { PublicServiceModel } from "../public-service-model.js";
|
|
2
|
+
import { PublicServiceDetailModel } from "../public-service-detail-model.js";
|
|
3
|
+
import { PublicAvailabilityModel } from "../public-availability-model.js";
|
|
4
|
+
import { PublicAppointmentModel } from "../public-appointment-model.js";
|
|
5
|
+
class BookingApiError extends Error {
|
|
6
|
+
constructor(kind, code, httpStatus, message, details) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.kind = kind;
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.httpStatus = httpStatus;
|
|
11
|
+
this.details = details;
|
|
12
|
+
this.name = "BookingApiError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function unwrapPublicRpc(wire, httpStatus) {
|
|
16
|
+
if ((wire == null ? void 0 : wire.success) === true && wire.msg !== void 0) {
|
|
17
|
+
return wire.msg;
|
|
18
|
+
}
|
|
19
|
+
throw new BookingApiError(
|
|
20
|
+
"biz",
|
|
21
|
+
(wire == null ? void 0 : wire.code) ?? "BOOKING_UNKNOWN_ERROR",
|
|
22
|
+
httpStatus,
|
|
23
|
+
(wire == null ? void 0 : wire.errMsg) ?? "Booking request failed",
|
|
24
|
+
wire
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
function normalizeBaseUrl(domain) {
|
|
28
|
+
return (domain ?? "").replace(/\/$/, "");
|
|
29
|
+
}
|
|
30
|
+
async function publicFetch(domain, path, init) {
|
|
31
|
+
const baseUrl = normalizeBaseUrl(domain);
|
|
32
|
+
let res;
|
|
33
|
+
try {
|
|
34
|
+
res = await fetch(`${baseUrl}${path}`, {
|
|
35
|
+
...init,
|
|
36
|
+
headers: {
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
...(init == null ? void 0 : init.headers) ?? {}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
} catch (err) {
|
|
42
|
+
throw new BookingApiError(
|
|
43
|
+
"network",
|
|
44
|
+
"NETWORK_ERROR",
|
|
45
|
+
null,
|
|
46
|
+
err instanceof Error ? err.message : "Network error"
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
let body = null;
|
|
51
|
+
try {
|
|
52
|
+
body = await res.json();
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
const code = res.status === 429 ? "RATE_LIMITED" : "INTERNAL_ERROR";
|
|
56
|
+
throw new BookingApiError(
|
|
57
|
+
"http",
|
|
58
|
+
code,
|
|
59
|
+
res.status,
|
|
60
|
+
`HTTP ${res.status}`,
|
|
61
|
+
body
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
let wire;
|
|
65
|
+
try {
|
|
66
|
+
wire = await res.json();
|
|
67
|
+
} catch (err) {
|
|
68
|
+
throw new BookingApiError(
|
|
69
|
+
"network",
|
|
70
|
+
"INVALID_JSON",
|
|
71
|
+
res.status,
|
|
72
|
+
err instanceof Error ? err.message : "Invalid JSON"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return unwrapPublicRpc(wire, res.status);
|
|
76
|
+
}
|
|
77
|
+
async function fetchServices(domain, brandSlug) {
|
|
78
|
+
const wire = await publicFetch(
|
|
79
|
+
domain,
|
|
80
|
+
`/api/public/booking/${encodeURIComponent(brandSlug)}/services`
|
|
81
|
+
);
|
|
82
|
+
return {
|
|
83
|
+
services: wire.services.map(PublicServiceModel.fromWire),
|
|
84
|
+
timezone: wire.timezone
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
async function fetchServiceDetail(domain, brandSlug, serviceId) {
|
|
88
|
+
const wire = await publicFetch(
|
|
89
|
+
domain,
|
|
90
|
+
`/api/public/booking/${encodeURIComponent(brandSlug)}/services/${serviceId}`
|
|
91
|
+
);
|
|
92
|
+
return {
|
|
93
|
+
service: PublicServiceDetailModel.fromWire(wire.service),
|
|
94
|
+
timezone: wire.timezone
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async function fetchAvailability(domain, brandSlug, params) {
|
|
98
|
+
const qs = new URLSearchParams({
|
|
99
|
+
service_id: String(params.service_id),
|
|
100
|
+
from: params.from,
|
|
101
|
+
to: params.to
|
|
102
|
+
});
|
|
103
|
+
const wire = await publicFetch(
|
|
104
|
+
domain,
|
|
105
|
+
`/api/public/booking/${encodeURIComponent(brandSlug)}/availability?${qs.toString()}`
|
|
106
|
+
);
|
|
107
|
+
return PublicAvailabilityModel.fromWire(wire);
|
|
108
|
+
}
|
|
109
|
+
async function submitAppointment(domain, brandSlug, body) {
|
|
110
|
+
const wireBody = {
|
|
111
|
+
service_id: body.service_id,
|
|
112
|
+
scheduled_at: body.scheduled_at,
|
|
113
|
+
contact: {
|
|
114
|
+
name: [body.customer.first_name, body.customer.last_name].filter(Boolean).join(" ").trim(),
|
|
115
|
+
email: body.customer.email,
|
|
116
|
+
phone: body.customer.phone,
|
|
117
|
+
metadata: {
|
|
118
|
+
first_name: body.customer.first_name,
|
|
119
|
+
last_name: body.customer.last_name,
|
|
120
|
+
address: body.customer.address
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
custom_answers: body.custom_answers.map((a) => ({
|
|
124
|
+
question_id: a.questionId,
|
|
125
|
+
label: a.label,
|
|
126
|
+
answer: a.answer
|
|
127
|
+
})),
|
|
128
|
+
source_page: body.source_page
|
|
129
|
+
};
|
|
130
|
+
const wire = await publicFetch(
|
|
131
|
+
domain,
|
|
132
|
+
`/api/public/booking/${encodeURIComponent(brandSlug)}/appointments`,
|
|
133
|
+
{
|
|
134
|
+
method: "POST",
|
|
135
|
+
body: JSON.stringify(wireBody)
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
return PublicAppointmentModel.fromWire(wire.appointment);
|
|
139
|
+
}
|
|
140
|
+
export {
|
|
141
|
+
BookingApiError,
|
|
142
|
+
fetchAvailability,
|
|
143
|
+
fetchServiceDetail,
|
|
144
|
+
fetchServices,
|
|
145
|
+
submitAppointment,
|
|
146
|
+
unwrapPublicRpc
|
|
147
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Input } from "@springbrand/site-block/components/shadcn/input";
|
|
4
|
+
import { Label } from "@springbrand/site-block/components/shadcn/label";
|
|
5
|
+
import { Textarea } from "@springbrand/site-block/components/shadcn/textarea";
|
|
6
|
+
import { RadioGroup, RadioGroupItem } from "@springbrand/site-block/components/shadcn/radio-group";
|
|
7
|
+
import { Checkbox } from "@springbrand/site-block/components/shadcn/checkbox";
|
|
8
|
+
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@springbrand/site-block/components/shadcn/select";
|
|
9
|
+
import { cn } from "@springbrand/site-block/lib/utils";
|
|
10
|
+
function BookingFormField({
|
|
11
|
+
question,
|
|
12
|
+
value,
|
|
13
|
+
onChange,
|
|
14
|
+
error
|
|
15
|
+
}) {
|
|
16
|
+
const fieldId = `q-${question.id}`;
|
|
17
|
+
const renderControl = () => {
|
|
18
|
+
switch (question.type) {
|
|
19
|
+
case "one_line":
|
|
20
|
+
return /* @__PURE__ */ jsx(
|
|
21
|
+
Input,
|
|
22
|
+
{
|
|
23
|
+
id: fieldId,
|
|
24
|
+
value: value ?? "",
|
|
25
|
+
onChange: (e) => onChange(e.target.value),
|
|
26
|
+
"aria-invalid": !!error || void 0
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
case "multi_line":
|
|
30
|
+
return /* @__PURE__ */ jsx(
|
|
31
|
+
Textarea,
|
|
32
|
+
{
|
|
33
|
+
id: fieldId,
|
|
34
|
+
value: value ?? "",
|
|
35
|
+
onChange: (e) => onChange(e.target.value),
|
|
36
|
+
"aria-invalid": !!error || void 0
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
case "radio":
|
|
40
|
+
return /* @__PURE__ */ jsx(
|
|
41
|
+
RadioGroup,
|
|
42
|
+
{
|
|
43
|
+
value: value ?? "",
|
|
44
|
+
onValueChange: (v) => onChange(v),
|
|
45
|
+
className: "gap-2",
|
|
46
|
+
children: (question.options ?? []).map((opt) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
47
|
+
/* @__PURE__ */ jsx(RadioGroupItem, { id: `${fieldId}-${opt.id}`, value: opt.id }),
|
|
48
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: `${fieldId}-${opt.id}`, className: "font-normal", children: opt.label })
|
|
49
|
+
] }, opt.id))
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
case "checkboxes": {
|
|
53
|
+
const arr = Array.isArray(value) ? value : [];
|
|
54
|
+
return /* @__PURE__ */ jsx("div", { className: "space-y-2", children: (question.options ?? []).map((opt) => {
|
|
55
|
+
const checked = arr.includes(opt.id);
|
|
56
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
57
|
+
/* @__PURE__ */ jsx(
|
|
58
|
+
Checkbox,
|
|
59
|
+
{
|
|
60
|
+
id: `${fieldId}-${opt.id}`,
|
|
61
|
+
checked,
|
|
62
|
+
onCheckedChange: (c) => {
|
|
63
|
+
const next = c ? [...arr, opt.id] : arr.filter((id) => id !== opt.id);
|
|
64
|
+
onChange(next);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
),
|
|
68
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: `${fieldId}-${opt.id}`, className: "font-normal", children: opt.label })
|
|
69
|
+
] }, opt.id);
|
|
70
|
+
}) });
|
|
71
|
+
}
|
|
72
|
+
case "dropdown": {
|
|
73
|
+
const options = question.options ?? [];
|
|
74
|
+
return /* @__PURE__ */ jsxs(
|
|
75
|
+
Select,
|
|
76
|
+
{
|
|
77
|
+
value: value ?? "",
|
|
78
|
+
items: options.map((opt) => ({ label: opt.label, value: opt.id })),
|
|
79
|
+
onValueChange: (v) => onChange(v),
|
|
80
|
+
children: [
|
|
81
|
+
/* @__PURE__ */ jsx(SelectTrigger, { id: fieldId, "aria-invalid": !!error || void 0, children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select…" }) }),
|
|
82
|
+
/* @__PURE__ */ jsx(SelectContent, { children: options.map((opt) => /* @__PURE__ */ jsx(SelectItem, { value: opt.id, children: opt.label }, opt.id)) })
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
default:
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("space-y-1.5", error && "[&>label]:text-destructive"), children: [
|
|
92
|
+
/* @__PURE__ */ jsxs(Label, { htmlFor: fieldId, children: [
|
|
93
|
+
question.label,
|
|
94
|
+
question.required && /* @__PURE__ */ jsx("span", { className: "ml-0.5 text-destructive", children: "*" })
|
|
95
|
+
] }),
|
|
96
|
+
renderControl(),
|
|
97
|
+
error && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: error })
|
|
98
|
+
] });
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
BookingFormField
|
|
102
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { X } from "lucide-react";
|
|
4
|
+
import { useState, useEffect } from "react";
|
|
5
|
+
import { Sheet, SheetContent } from "@springbrand/site-block/components/shadcn/sheet";
|
|
6
|
+
import { Button } from "@springbrand/site-block/components/shadcn/button";
|
|
7
|
+
import { cn } from "@springbrand/site-block/lib/utils";
|
|
8
|
+
function BubbleContainer({
|
|
9
|
+
open,
|
|
10
|
+
onOpenChange,
|
|
11
|
+
title,
|
|
12
|
+
children,
|
|
13
|
+
onBack,
|
|
14
|
+
stepIndicator,
|
|
15
|
+
className,
|
|
16
|
+
container
|
|
17
|
+
}) {
|
|
18
|
+
const [isDesktop, setIsDesktop] = useState(false);
|
|
19
|
+
const headerTitle = title ?? (onBack ? null : "Book");
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const targetWindow = resolveContainerWindow(container);
|
|
22
|
+
if (!targetWindow) return;
|
|
23
|
+
const mq = targetWindow.matchMedia("(min-width: 768px)");
|
|
24
|
+
const update = () => setIsDesktop(mq.matches);
|
|
25
|
+
update();
|
|
26
|
+
mq.addEventListener("change", update);
|
|
27
|
+
return () => mq.removeEventListener("change", update);
|
|
28
|
+
}, [container]);
|
|
29
|
+
return /* @__PURE__ */ jsx(Sheet, { open, onOpenChange, children: /* @__PURE__ */ jsxs(
|
|
30
|
+
SheetContent,
|
|
31
|
+
{
|
|
32
|
+
side: isDesktop ? "right" : "bottom",
|
|
33
|
+
showCloseButton: false,
|
|
34
|
+
container,
|
|
35
|
+
className: cn(
|
|
36
|
+
"z-[1001] p-0 flex flex-col gap-0 overflow-hidden",
|
|
37
|
+
isDesktop ? (
|
|
38
|
+
// 桌面 popover 与气泡按钮的相对位置:
|
|
39
|
+
// - bottom-28 (112px) 让对话框底端高于按钮顶部 (72px) 约 40px,留出明显呼吸感
|
|
40
|
+
// - right-8 (32px) 把对话框右边再让出一点,避免和屏幕右沿/按钮贴在一起
|
|
41
|
+
// SheetContent 基类带 data-[side=right]:{inset-y-0,h-full,w-3/4},
|
|
42
|
+
// 必须用 ! important 覆盖 top / bottom / 高宽,否则会被钉在 top:0 全高。
|
|
43
|
+
"w-[400px]! sm:max-w-[420px] right-8 top-auto! bottom-28! h-[600px]! max-h-[calc(100vh-9rem)] rounded-2xl border shadow-2xl"
|
|
44
|
+
) : "h-[min(85svh,calc(100dvh-env(safe-area-inset-top)-0.75rem))] w-full rounded-t-2xl border-t",
|
|
45
|
+
className
|
|
46
|
+
),
|
|
47
|
+
overlayClassName: "z-[1000]",
|
|
48
|
+
children: [
|
|
49
|
+
/* @__PURE__ */ jsxs("header", { className: "flex items-center justify-between border-b px-4 h-12 shrink-0", children: [
|
|
50
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [
|
|
51
|
+
onBack && /* @__PURE__ */ jsx(
|
|
52
|
+
Button,
|
|
53
|
+
{
|
|
54
|
+
type: "button",
|
|
55
|
+
variant: "ghost",
|
|
56
|
+
size: "sm",
|
|
57
|
+
className: "-ml-2 h-8 px-2",
|
|
58
|
+
onClick: onBack,
|
|
59
|
+
children: "Back"
|
|
60
|
+
}
|
|
61
|
+
),
|
|
62
|
+
headerTitle && /* @__PURE__ */ jsx("span", { className: "font-semibold text-sm truncate", children: headerTitle })
|
|
63
|
+
] }),
|
|
64
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [
|
|
65
|
+
stepIndicator && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: stepIndicator }),
|
|
66
|
+
/* @__PURE__ */ jsx(
|
|
67
|
+
"button",
|
|
68
|
+
{
|
|
69
|
+
type: "button",
|
|
70
|
+
"aria-label": "Close",
|
|
71
|
+
onClick: () => onOpenChange(false),
|
|
72
|
+
className: "rounded-md p-1 text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
73
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
] })
|
|
77
|
+
] }),
|
|
78
|
+
/* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto overscroll-contain [-webkit-overflow-scrolling:touch]", children })
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
) });
|
|
82
|
+
}
|
|
83
|
+
function resolveContainerWindow(container) {
|
|
84
|
+
const element = container && "current" in container ? container.current : container;
|
|
85
|
+
return (element == null ? void 0 : element.ownerDocument.defaultView) ?? null;
|
|
86
|
+
}
|
|
87
|
+
export {
|
|
88
|
+
BubbleContainer
|
|
89
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Calendar } from "lucide-react";
|
|
4
|
+
import { Button } from "@springbrand/site-block/components/shadcn/button";
|
|
5
|
+
import { cn } from "@springbrand/site-block/lib/utils";
|
|
6
|
+
function BubbleTrigger({
|
|
7
|
+
label = "Book now",
|
|
8
|
+
onClick,
|
|
9
|
+
editorMode,
|
|
10
|
+
className,
|
|
11
|
+
style,
|
|
12
|
+
...domProps
|
|
13
|
+
}) {
|
|
14
|
+
return /* @__PURE__ */ jsx(
|
|
15
|
+
"div",
|
|
16
|
+
{
|
|
17
|
+
...domProps,
|
|
18
|
+
className: cn(
|
|
19
|
+
"fixed bottom-[calc(env(safe-area-inset-bottom)+1rem)] right-[calc(env(safe-area-inset-right)+1rem)] z-50 print:hidden sm:bottom-6 sm:right-6",
|
|
20
|
+
editorMode && "group cursor-pointer outline-primary/40 rounded-full"
|
|
21
|
+
),
|
|
22
|
+
onClick,
|
|
23
|
+
children: /* @__PURE__ */ jsxs(
|
|
24
|
+
Button,
|
|
25
|
+
{
|
|
26
|
+
type: "button",
|
|
27
|
+
size: "lg",
|
|
28
|
+
className: cn("rounded-full shadow-lg gap-2 px-5 h-12", className),
|
|
29
|
+
style,
|
|
30
|
+
children: [
|
|
31
|
+
/* @__PURE__ */ jsx(Calendar, { className: "h-5 w-5" }),
|
|
32
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: label })
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
BubbleTrigger
|
|
41
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import dayjs from "dayjs";
|
|
2
|
+
import { default as default2 } from "dayjs";
|
|
3
|
+
import utc from "../../../node_modules/.pnpm/dayjs@1.11.20/node_modules/dayjs/plugin/utc.js";
|
|
4
|
+
import timezone from "../../../node_modules/.pnpm/dayjs@1.11.20/node_modules/dayjs/plugin/timezone.js";
|
|
5
|
+
dayjs.extend(utc);
|
|
6
|
+
dayjs.extend(timezone);
|
|
7
|
+
export {
|
|
8
|
+
default2 as default
|
|
9
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useRef } from "react";
|
|
4
|
+
import { useRuntimeHost } from "../runtime-host-provider/index.js";
|
|
5
|
+
import { BubbleContainer } from "./bubble-container.js";
|
|
6
|
+
import { BubbleTrigger } from "./bubble-trigger.js";
|
|
7
|
+
import { Step1ServicePicker } from "./step-1-service-picker.js";
|
|
8
|
+
import { Step2TimePicker } from "./step-2-time-picker.js";
|
|
9
|
+
import { Step3DetailsForm } from "./step-3-details-form.js";
|
|
10
|
+
import { Step4Confirm } from "./step-4-confirm.js";
|
|
11
|
+
import { useBookingFlow } from "./use-booking-flow.js";
|
|
12
|
+
function FloatingBookingBubble({
|
|
13
|
+
text = "Book now",
|
|
14
|
+
styles: _styles,
|
|
15
|
+
onClick,
|
|
16
|
+
...triggerProps
|
|
17
|
+
} = {}) {
|
|
18
|
+
var _a, _b;
|
|
19
|
+
const { isEdit, bookingBubbleVisible } = useRuntimeHost();
|
|
20
|
+
const flow = useBookingFlow();
|
|
21
|
+
const [open, setOpen] = useState(false);
|
|
22
|
+
const portalAnchorRef = useRef(null);
|
|
23
|
+
if (bookingBubbleVisible !== true) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const phase = flow.phase;
|
|
27
|
+
if (isEdit) {
|
|
28
|
+
return /* @__PURE__ */ jsx(
|
|
29
|
+
BubbleTrigger,
|
|
30
|
+
{
|
|
31
|
+
...triggerProps,
|
|
32
|
+
label: text,
|
|
33
|
+
editorMode: true,
|
|
34
|
+
onClick: (e) => {
|
|
35
|
+
onClick == null ? void 0 : onClick(e);
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
e.stopPropagation();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const stepIndex = (() => {
|
|
43
|
+
switch (phase) {
|
|
44
|
+
case "step1":
|
|
45
|
+
return 1;
|
|
46
|
+
case "step2":
|
|
47
|
+
return 2;
|
|
48
|
+
case "step3":
|
|
49
|
+
return 3;
|
|
50
|
+
case "step4":
|
|
51
|
+
case "success":
|
|
52
|
+
return 4;
|
|
53
|
+
default:
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
})();
|
|
57
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
58
|
+
/* @__PURE__ */ jsx(
|
|
59
|
+
BubbleTrigger,
|
|
60
|
+
{
|
|
61
|
+
...triggerProps,
|
|
62
|
+
label: text,
|
|
63
|
+
onClick: (e) => {
|
|
64
|
+
onClick == null ? void 0 : onClick(e);
|
|
65
|
+
flow.open();
|
|
66
|
+
setOpen(true);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
),
|
|
70
|
+
/* @__PURE__ */ jsx("div", { ref: portalAnchorRef }),
|
|
71
|
+
/* @__PURE__ */ jsxs(
|
|
72
|
+
BubbleContainer,
|
|
73
|
+
{
|
|
74
|
+
open: open && phase !== "idle",
|
|
75
|
+
onOpenChange: (next) => {
|
|
76
|
+
setOpen(next);
|
|
77
|
+
if (!next) flow.close();
|
|
78
|
+
},
|
|
79
|
+
title: phase === "success" ? "Booking Confirmed" : void 0,
|
|
80
|
+
onBack: phase === "step2" || phase === "step3" || phase === "step4" ? flow.back : void 0,
|
|
81
|
+
stepIndicator: stepIndex ? `${stepIndex} / 4` : void 0,
|
|
82
|
+
container: portalAnchorRef,
|
|
83
|
+
children: [
|
|
84
|
+
phase === "step1" && /* @__PURE__ */ jsx(
|
|
85
|
+
Step1ServicePicker,
|
|
86
|
+
{
|
|
87
|
+
services: flow.services,
|
|
88
|
+
loading: flow.servicesLoading,
|
|
89
|
+
error: flow.servicesError,
|
|
90
|
+
onPick: flow.pickService,
|
|
91
|
+
onRetry: () => flow.open()
|
|
92
|
+
}
|
|
93
|
+
),
|
|
94
|
+
phase === "step2" && flow.selectedService && /* @__PURE__ */ jsx(
|
|
95
|
+
Step2TimePicker,
|
|
96
|
+
{
|
|
97
|
+
visibleMonth: flow.visibleMonth,
|
|
98
|
+
availability: flow.availability,
|
|
99
|
+
timezone: ((_a = flow.availability) == null ? void 0 : _a.timezone) ?? "",
|
|
100
|
+
loading: flow.availabilityLoading,
|
|
101
|
+
error: flow.availabilityError,
|
|
102
|
+
onChangeMonth: flow.changeMonth,
|
|
103
|
+
onPickSlot: flow.pickSlot,
|
|
104
|
+
onRetry: () => flow.changeMonth(flow.visibleMonth)
|
|
105
|
+
}
|
|
106
|
+
),
|
|
107
|
+
phase === "step3" && flow.selectedService && /* @__PURE__ */ jsx(
|
|
108
|
+
Step3DetailsForm,
|
|
109
|
+
{
|
|
110
|
+
service: flow.selectedService,
|
|
111
|
+
form: flow.form,
|
|
112
|
+
onChange: flow.setForm,
|
|
113
|
+
onNext: () => flow.submit()
|
|
114
|
+
}
|
|
115
|
+
),
|
|
116
|
+
(phase === "step4" || phase === "success") && flow.selectedService && flow.selectedSlot && /* @__PURE__ */ jsx(
|
|
117
|
+
Step4Confirm,
|
|
118
|
+
{
|
|
119
|
+
service: flow.selectedService,
|
|
120
|
+
slot: flow.selectedSlot,
|
|
121
|
+
timezone: ((_b = flow.availability) == null ? void 0 : _b.timezone) ?? "",
|
|
122
|
+
form: flow.form,
|
|
123
|
+
submitting: flow.submitting,
|
|
124
|
+
submitError: flow.submitError,
|
|
125
|
+
confirmation: flow.confirmation,
|
|
126
|
+
onSubmit: flow.submit,
|
|
127
|
+
onClose: () => {
|
|
128
|
+
setOpen(false);
|
|
129
|
+
flow.close();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
] });
|
|
137
|
+
}
|
|
138
|
+
const FloatingBookingBubbleDefaults = {
|
|
139
|
+
text: "Book now",
|
|
140
|
+
styles: {}
|
|
141
|
+
};
|
|
142
|
+
export {
|
|
143
|
+
FloatingBookingBubble,
|
|
144
|
+
FloatingBookingBubbleDefaults,
|
|
145
|
+
FloatingBookingBubble as default
|
|
146
|
+
};
|