@flamingo-stack/openframe-frontend-core 0.0.574 → 0.0.575-snapshot.20260827225444
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/components/meeting-scheduler/index.cjs +105 -98
- package/dist/components/meeting-scheduler/index.cjs.map +1 -1
- package/dist/components/meeting-scheduler/index.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/index.js +103 -96
- package/dist/components/meeting-scheduler/index.js.map +1 -1
- package/dist/hooks/use-meeting-booking.d.ts +1 -1
- package/dist/hooks/use-meeting-booking.d.ts.map +1 -1
- package/dist/schemas/meeting-booking-schema.cjs +9 -0
- package/dist/schemas/meeting-booking-schema.cjs.map +1 -1
- package/dist/schemas/meeting-booking-schema.d.ts +10 -1
- package/dist/schemas/meeting-booking-schema.d.ts.map +1 -1
- package/dist/schemas/meeting-booking-schema.js +9 -1
- package/dist/schemas/meeting-booking-schema.js.map +1 -1
- package/package.json +1 -1
- package/src/components/meeting-scheduler/index.tsx +16 -14
- package/src/hooks/use-meeting-booking.ts +6 -13
- package/src/schemas/meeting-booking-schema.ts +18 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/meeting-scheduler/index.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/meeting-scheduler/index.tsx"],"names":[],"mappings":"AA0CA,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EACjB,MAAM,sCAAsC,CAAC;AA4B9C,MAAM,WAAW,4BAA4B;IAC3C,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;IAClB,2FAA2F;IAC3F,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,mEAAmE;IACnE,KAAK,CAAC,EAAE,WAAW,EAAE,CAAC;IACtB,sFAAsF;IACtF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,yFAAyF;IACzF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAqCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,eAAO,MAAM,mBAAmB,qCAAqC,CAAC;AAoEtE,wBAAgB,uBAAuB,CAAC,EACtC,SAAS,EACT,UAAe,EACf,mBAAmB,EACnB,KAAK,EACL,WAAW,EACX,KAAK,EACL,eAAe,EACf,WAAW,EACX,MAAM,EACN,QAAQ,EACR,SAAS,GACV,EAAE,4BAA4B,+BAoV9B;AAED,OAAO,EACL,yBAAyB,EACzB,oCAAoC,EACpC,KAAK,8BAA8B,GACpC,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,WAAW,EAAE,CAAC;AAC/F,YAAY,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC"}
|
|
@@ -26,7 +26,8 @@ import {
|
|
|
26
26
|
import "../../chunk-L2SLFCXP.js";
|
|
27
27
|
import "../../chunk-BJXDHKUX.js";
|
|
28
28
|
import {
|
|
29
|
-
useHumanitySignals
|
|
29
|
+
useHumanitySignals,
|
|
30
|
+
useToast
|
|
30
31
|
} from "../../chunk-HM2R5MDR.js";
|
|
31
32
|
import "../../chunk-ZRIVCLPR.js";
|
|
32
33
|
import "../../chunk-T5DAFFK7.js";
|
|
@@ -79,102 +80,17 @@ import { useCallback as useCallback3, useEffect as useEffect2, useMemo as useMem
|
|
|
79
80
|
// src/hooks/use-meeting-booking.ts
|
|
80
81
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
81
82
|
import { useCallback, useLayoutEffect, useRef, useState } from "react";
|
|
82
|
-
|
|
83
|
+
|
|
84
|
+
// src/schemas/meeting-booking-schema.ts
|
|
85
|
+
import { z } from "zod";
|
|
86
|
+
var MEETING_BOOKING_ERROR_CODES = [
|
|
83
87
|
"SLOT_TAKEN",
|
|
84
88
|
"VALIDATION",
|
|
89
|
+
"INVALID_EMAIL",
|
|
85
90
|
"LINK_GONE",
|
|
86
91
|
"TEMPORARILY_UNAVAILABLE",
|
|
87
92
|
"MEETING_UNAVAILABLE"
|
|
88
93
|
];
|
|
89
|
-
var AVAILABILITY_STALE_MS = 0;
|
|
90
|
-
function meetingAvailabilityKey(apiBaseUrl, meetingId, monthOffset) {
|
|
91
|
-
return ["meeting-availability", apiBaseUrl, meetingId, monthOffset];
|
|
92
|
-
}
|
|
93
|
-
function useMeetingBooking(options) {
|
|
94
|
-
const { meetingId, apiBaseUrl = "", initialAvailability } = options;
|
|
95
|
-
const queryClient = useQueryClient();
|
|
96
|
-
const seededOffset = initialAvailability?.meetingId === meetingId ? initialAvailability.monthOffset : void 0;
|
|
97
|
-
const [monthOffset, setMonthOffsetState] = useState(seededOffset ?? 0);
|
|
98
|
-
const setMonthOffset = useCallback((offset) => {
|
|
99
|
-
setMonthOffsetState(Math.max(0, Math.min(MAX_MONTH_OFFSET, offset)));
|
|
100
|
-
}, []);
|
|
101
|
-
const prevMeetingIdRef = useRef(meetingId);
|
|
102
|
-
useLayoutEffect(() => {
|
|
103
|
-
if (prevMeetingIdRef.current === meetingId) return;
|
|
104
|
-
prevMeetingIdRef.current = meetingId;
|
|
105
|
-
setMonthOffsetState(0);
|
|
106
|
-
}, [meetingId]);
|
|
107
|
-
const seed = initialAvailability?.meetingId === meetingId && initialAvailability.monthOffset === monthOffset ? initialAvailability : void 0;
|
|
108
|
-
const availabilityQuery = useQuery({
|
|
109
|
-
queryKey: meetingAvailabilityKey(apiBaseUrl, meetingId, monthOffset),
|
|
110
|
-
queryFn: async ({ signal }) => {
|
|
111
|
-
const params = new URLSearchParams({ meeting: meetingId, monthOffset: String(monthOffset) });
|
|
112
|
-
const res = await contentFetch(`${apiBaseUrl}/api/meetings/availability?${params}`, { signal });
|
|
113
|
-
if (!res.ok) throw new Error(`availability ${res.status}`);
|
|
114
|
-
return await res.json();
|
|
115
|
-
},
|
|
116
|
-
staleTime: AVAILABILITY_STALE_MS,
|
|
117
|
-
initialData: seed,
|
|
118
|
-
// Stamped as infinitely old on purpose: the seed is first-paint
|
|
119
|
-
// scaffolding for a 60-second-volatile resource, not truth, so it paints
|
|
120
|
-
// immediately AND is refetched immediately.
|
|
121
|
-
initialDataUpdatedAt: seed ? 0 : void 0
|
|
122
|
-
});
|
|
123
|
-
const refetchAvailability = useCallback(async () => {
|
|
124
|
-
await queryClient.invalidateQueries({ queryKey: meetingAvailabilityKey(apiBaseUrl, meetingId, monthOffset) }).catch(() => void 0);
|
|
125
|
-
}, [queryClient, apiBaseUrl, meetingId, monthOffset]);
|
|
126
|
-
const bookMutation = useMutation({
|
|
127
|
-
mutationFn: async (payload) => {
|
|
128
|
-
try {
|
|
129
|
-
const res = await contentFetch(`${apiBaseUrl}/api/meetings/book`, {
|
|
130
|
-
method: "POST",
|
|
131
|
-
headers: { "Content-Type": "application/json" },
|
|
132
|
-
body: JSON.stringify(payload)
|
|
133
|
-
});
|
|
134
|
-
const data = await res.json().catch(() => ({}));
|
|
135
|
-
if (res.ok) return { ok: true, confirmation: data };
|
|
136
|
-
const rawCode = typeof data.code === "string" ? data.code : "";
|
|
137
|
-
const code = KNOWN_CODES.includes(rawCode) ? rawCode : "TEMPORARILY_UNAVAILABLE";
|
|
138
|
-
return { ok: false, code, message: typeof data.error === "string" ? data.error : void 0 };
|
|
139
|
-
} catch {
|
|
140
|
-
return { ok: false, code: "TEMPORARILY_UNAVAILABLE" };
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
// A booking is not a read — never retried, at any layer.
|
|
144
|
-
retry: false
|
|
145
|
-
});
|
|
146
|
-
const submittingRef = useRef(null);
|
|
147
|
-
const { mutateAsync } = bookMutation;
|
|
148
|
-
const book = useCallback(
|
|
149
|
-
async (payload) => {
|
|
150
|
-
if (submittingRef.current !== null) return { ok: false, code: "VALIDATION", message: "Already submitting" };
|
|
151
|
-
const submitToken = {};
|
|
152
|
-
submittingRef.current = submitToken;
|
|
153
|
-
try {
|
|
154
|
-
return await mutateAsync(payload);
|
|
155
|
-
} finally {
|
|
156
|
-
if (submittingRef.current === submitToken) submittingRef.current = null;
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
[mutateAsync]
|
|
160
|
-
);
|
|
161
|
-
return {
|
|
162
|
-
availability: availabilityQuery.data ?? null,
|
|
163
|
-
/** No availability for this month yet — the times column has nothing to show. */
|
|
164
|
-
isLoadingAvailability: availabilityQuery.isPending,
|
|
165
|
-
/** ANY request in flight, including a silent revalidation of cached data. */
|
|
166
|
-
isFetchingAvailability: availabilityQuery.isFetching,
|
|
167
|
-
availabilityError: availabilityQuery.error ? availabilityQuery.error.message : null,
|
|
168
|
-
monthOffset,
|
|
169
|
-
setMonthOffset,
|
|
170
|
-
refetchAvailability,
|
|
171
|
-
book,
|
|
172
|
-
isSubmitting: bookMutation.isPending
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// src/schemas/meeting-booking-schema.ts
|
|
177
|
-
import { z } from "zod";
|
|
178
94
|
var SUPPORTED_FORM_FIELD_TYPES = ["text", "textarea", "select", "radio", "checkbox"];
|
|
179
95
|
function isSupportedFormField(field) {
|
|
180
96
|
return SUPPORTED_FORM_FIELD_TYPES.includes(field.type);
|
|
@@ -261,6 +177,94 @@ function makeBookingSchema(formFields, legalConsent) {
|
|
|
261
177
|
});
|
|
262
178
|
}
|
|
263
179
|
|
|
180
|
+
// src/hooks/use-meeting-booking.ts
|
|
181
|
+
var AVAILABILITY_STALE_MS = 0;
|
|
182
|
+
function meetingAvailabilityKey(apiBaseUrl, meetingId, monthOffset) {
|
|
183
|
+
return ["meeting-availability", apiBaseUrl, meetingId, monthOffset];
|
|
184
|
+
}
|
|
185
|
+
function useMeetingBooking(options) {
|
|
186
|
+
const { meetingId, apiBaseUrl = "", initialAvailability } = options;
|
|
187
|
+
const queryClient = useQueryClient();
|
|
188
|
+
const seededOffset = initialAvailability?.meetingId === meetingId ? initialAvailability.monthOffset : void 0;
|
|
189
|
+
const [monthOffset, setMonthOffsetState] = useState(seededOffset ?? 0);
|
|
190
|
+
const setMonthOffset = useCallback((offset) => {
|
|
191
|
+
setMonthOffsetState(Math.max(0, Math.min(MAX_MONTH_OFFSET, offset)));
|
|
192
|
+
}, []);
|
|
193
|
+
const prevMeetingIdRef = useRef(meetingId);
|
|
194
|
+
useLayoutEffect(() => {
|
|
195
|
+
if (prevMeetingIdRef.current === meetingId) return;
|
|
196
|
+
prevMeetingIdRef.current = meetingId;
|
|
197
|
+
setMonthOffsetState(0);
|
|
198
|
+
}, [meetingId]);
|
|
199
|
+
const seed = initialAvailability?.meetingId === meetingId && initialAvailability.monthOffset === monthOffset ? initialAvailability : void 0;
|
|
200
|
+
const availabilityQuery = useQuery({
|
|
201
|
+
queryKey: meetingAvailabilityKey(apiBaseUrl, meetingId, monthOffset),
|
|
202
|
+
queryFn: async ({ signal }) => {
|
|
203
|
+
const params = new URLSearchParams({ meeting: meetingId, monthOffset: String(monthOffset) });
|
|
204
|
+
const res = await contentFetch(`${apiBaseUrl}/api/meetings/availability?${params}`, { signal });
|
|
205
|
+
if (!res.ok) throw new Error(`availability ${res.status}`);
|
|
206
|
+
return await res.json();
|
|
207
|
+
},
|
|
208
|
+
staleTime: AVAILABILITY_STALE_MS,
|
|
209
|
+
initialData: seed,
|
|
210
|
+
// Stamped as infinitely old on purpose: the seed is first-paint
|
|
211
|
+
// scaffolding for a 60-second-volatile resource, not truth, so it paints
|
|
212
|
+
// immediately AND is refetched immediately.
|
|
213
|
+
initialDataUpdatedAt: seed ? 0 : void 0
|
|
214
|
+
});
|
|
215
|
+
const refetchAvailability = useCallback(async () => {
|
|
216
|
+
await queryClient.invalidateQueries({ queryKey: meetingAvailabilityKey(apiBaseUrl, meetingId, monthOffset) }).catch(() => void 0);
|
|
217
|
+
}, [queryClient, apiBaseUrl, meetingId, monthOffset]);
|
|
218
|
+
const bookMutation = useMutation({
|
|
219
|
+
mutationFn: async (payload) => {
|
|
220
|
+
try {
|
|
221
|
+
const res = await contentFetch(`${apiBaseUrl}/api/meetings/book`, {
|
|
222
|
+
method: "POST",
|
|
223
|
+
headers: { "Content-Type": "application/json" },
|
|
224
|
+
body: JSON.stringify(payload)
|
|
225
|
+
});
|
|
226
|
+
const data = await res.json().catch(() => ({}));
|
|
227
|
+
if (res.ok) return { ok: true, confirmation: data };
|
|
228
|
+
const rawCode = typeof data.code === "string" ? data.code : "";
|
|
229
|
+
const code = MEETING_BOOKING_ERROR_CODES.includes(rawCode) ? rawCode : "TEMPORARILY_UNAVAILABLE";
|
|
230
|
+
return { ok: false, code, message: typeof data.error === "string" ? data.error : void 0 };
|
|
231
|
+
} catch {
|
|
232
|
+
return { ok: false, code: "TEMPORARILY_UNAVAILABLE" };
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
// A booking is not a read — never retried, at any layer.
|
|
236
|
+
retry: false
|
|
237
|
+
});
|
|
238
|
+
const submittingRef = useRef(null);
|
|
239
|
+
const { mutateAsync } = bookMutation;
|
|
240
|
+
const book = useCallback(
|
|
241
|
+
async (payload) => {
|
|
242
|
+
if (submittingRef.current !== null) return { ok: false, code: "VALIDATION", message: "Already submitting" };
|
|
243
|
+
const submitToken = {};
|
|
244
|
+
submittingRef.current = submitToken;
|
|
245
|
+
try {
|
|
246
|
+
return await mutateAsync(payload);
|
|
247
|
+
} finally {
|
|
248
|
+
if (submittingRef.current === submitToken) submittingRef.current = null;
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
[mutateAsync]
|
|
252
|
+
);
|
|
253
|
+
return {
|
|
254
|
+
availability: availabilityQuery.data ?? null,
|
|
255
|
+
/** No availability for this month yet — the times column has nothing to show. */
|
|
256
|
+
isLoadingAvailability: availabilityQuery.isPending,
|
|
257
|
+
/** ANY request in flight, including a silent revalidation of cached data. */
|
|
258
|
+
isFetchingAvailability: availabilityQuery.isFetching,
|
|
259
|
+
availabilityError: availabilityQuery.error ? availabilityQuery.error.message : null,
|
|
260
|
+
monthOffset,
|
|
261
|
+
setMonthOffset,
|
|
262
|
+
refetchAvailability,
|
|
263
|
+
book,
|
|
264
|
+
isSubmitting: bookMutation.isPending
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
264
268
|
// src/components/meeting-scheduler/booking-form.tsx
|
|
265
269
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
266
270
|
import { useMemo } from "react";
|
|
@@ -1076,6 +1080,7 @@ function HubSpotMeetingScheduler({
|
|
|
1076
1080
|
const [confirmation, setConfirmation] = useState3(null);
|
|
1077
1081
|
const [bookingError, setBookingError] = useState3(null);
|
|
1078
1082
|
const { honeypotInputProps, getSignals, resetSignals } = useHumanitySignals();
|
|
1083
|
+
const { toast } = useToast();
|
|
1079
1084
|
const autoAdvanceCount = useRef2(0);
|
|
1080
1085
|
const [machineFor, setMachineFor] = useState3(meetingId);
|
|
1081
1086
|
if (machineFor !== meetingId) {
|
|
@@ -1137,9 +1142,15 @@ function HubSpotMeetingScheduler({
|
|
|
1137
1142
|
setStep("slot");
|
|
1138
1143
|
resetSignals();
|
|
1139
1144
|
void refetchAvailability();
|
|
1145
|
+
} else {
|
|
1146
|
+
toast({
|
|
1147
|
+
variant: "error",
|
|
1148
|
+
title: "Booking failed",
|
|
1149
|
+
description: BOOKING_ERROR_COPY[code] ?? BOOKING_ERROR_COPY.VALIDATION
|
|
1150
|
+
});
|
|
1140
1151
|
}
|
|
1141
1152
|
},
|
|
1142
|
-
[book, onBooked, refetchAvailability, resetSignals]
|
|
1153
|
+
[book, onBooked, refetchAvailability, resetSignals, toast]
|
|
1143
1154
|
);
|
|
1144
1155
|
const escapeHatch = fallbackUrl ? /* @__PURE__ */ jsx6(Button, { variant: "outline", size: "small-legacy", href: fallbackUrl, openInNewTab: true, children: "Open in HubSpot" }) : null;
|
|
1145
1156
|
if (isLoadingAvailability && !availability) {
|
|
@@ -1198,10 +1209,6 @@ function HubSpotMeetingScheduler({
|
|
|
1198
1209
|
"\xB7 ",
|
|
1199
1210
|
formatDurationCompact(durationMs / 1e3)
|
|
1200
1211
|
] }),
|
|
1201
|
-
bookingError && bookingError !== "SLOT_TAKEN" && /* @__PURE__ */ jsxs6(Alert, { variant: "error", className: "flex flex-col items-start gap-[var(--spacing-system-xs)]", children: [
|
|
1202
|
-
/* @__PURE__ */ jsx6(AlertDescription, { children: BOOKING_ERROR_COPY[bookingError] ?? BOOKING_ERROR_COPY.VALIDATION }),
|
|
1203
|
-
escapeHatch
|
|
1204
|
-
] }),
|
|
1205
1212
|
/* @__PURE__ */ jsx6(
|
|
1206
1213
|
BookingForm,
|
|
1207
1214
|
{
|