@flamingo-stack/openframe-frontend-core 0.0.574-snapshot.20260827223923 → 0.0.574
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 +93 -96
- package/dist/components/meeting-scheduler/index.cjs.map +1 -1
- package/dist/components/meeting-scheduler/index.js +90 -93
- 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 +0 -9
- package/dist/schemas/meeting-booking-schema.cjs.map +1 -1
- package/dist/schemas/meeting-booking-schema.d.ts +1 -10
- package/dist/schemas/meeting-booking-schema.d.ts.map +1 -1
- package/dist/schemas/meeting-booking-schema.js +1 -9
- package/dist/schemas/meeting-booking-schema.js.map +1 -1
- package/package.json +1 -1
- package/src/hooks/use-meeting-booking.ts +13 -6
- package/src/schemas/meeting-booking-schema.ts +2 -18
|
@@ -79,104 +79,13 @@ import { useCallback as useCallback3, useEffect as useEffect2, useMemo as useMem
|
|
|
79
79
|
// src/hooks/use-meeting-booking.ts
|
|
80
80
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
81
81
|
import { useCallback, useLayoutEffect, useRef, useState } from "react";
|
|
82
|
-
|
|
83
|
-
// src/schemas/meeting-booking-schema.ts
|
|
84
|
-
import { z } from "zod";
|
|
85
|
-
var MEETING_BOOKING_ERROR_CODES = [
|
|
82
|
+
var KNOWN_CODES = [
|
|
86
83
|
"SLOT_TAKEN",
|
|
87
84
|
"VALIDATION",
|
|
88
|
-
"INVALID_EMAIL",
|
|
89
85
|
"LINK_GONE",
|
|
90
86
|
"TEMPORARILY_UNAVAILABLE",
|
|
91
87
|
"MEETING_UNAVAILABLE"
|
|
92
88
|
];
|
|
93
|
-
var SUPPORTED_FORM_FIELD_TYPES = ["text", "textarea", "select", "radio", "checkbox"];
|
|
94
|
-
function isSupportedFormField(field) {
|
|
95
|
-
return SUPPORTED_FORM_FIELD_TYPES.includes(field.type);
|
|
96
|
-
}
|
|
97
|
-
var IANA_TZ_RE = /^(?:UTC|[A-Za-z_]+(?:\/[A-Za-z0-9_+-]+)+)$/;
|
|
98
|
-
function isValidIanaTimezone(tz) {
|
|
99
|
-
if (!IANA_TZ_RE.test(tz)) return false;
|
|
100
|
-
try {
|
|
101
|
-
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
102
|
-
return true;
|
|
103
|
-
} catch {
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
function isValidBcp47Locale(locale) {
|
|
108
|
-
try {
|
|
109
|
-
return Intl.getCanonicalLocales(locale).length > 0;
|
|
110
|
-
} catch {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
function makeBookingSchema(formFields, legalConsent) {
|
|
115
|
-
const answers = {};
|
|
116
|
-
for (const field of formFields) {
|
|
117
|
-
if (!isSupportedFormField(field)) continue;
|
|
118
|
-
let validator;
|
|
119
|
-
switch (field.type) {
|
|
120
|
-
case "checkbox":
|
|
121
|
-
validator = z.boolean();
|
|
122
|
-
break;
|
|
123
|
-
case "select":
|
|
124
|
-
case "radio":
|
|
125
|
-
validator = z.string().refine((v) => !v || (field.options ?? []).includes(v), {
|
|
126
|
-
message: `Please choose a valid option for ${field.label}`
|
|
127
|
-
});
|
|
128
|
-
break;
|
|
129
|
-
case "textarea":
|
|
130
|
-
validator = z.string().max(5e3, { message: `${field.label} is too long` });
|
|
131
|
-
break;
|
|
132
|
-
case "text":
|
|
133
|
-
default:
|
|
134
|
-
validator = z.string().max(1e3, { message: `${field.label} is too long` });
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
if (field.required) {
|
|
138
|
-
validator = field.type === "checkbox" ? z.literal(true, { message: `${field.label} is required` }) : validator.min(1, { message: `${field.label} is required` });
|
|
139
|
-
} else if (field.type !== "checkbox") {
|
|
140
|
-
validator = validator.optional().or(z.literal(""));
|
|
141
|
-
} else {
|
|
142
|
-
validator = z.boolean().optional();
|
|
143
|
-
}
|
|
144
|
-
answers[field.name] = validator;
|
|
145
|
-
}
|
|
146
|
-
const requiredConsentIds = (legalConsent?.communicationConsentCheckboxes ?? []).filter((c) => c.required).map((c) => c.communicationTypeId);
|
|
147
|
-
const hasRequiredAnswers = formFields.some((f) => isSupportedFormField(f) && f.required);
|
|
148
|
-
const answersObject = z.object(answers);
|
|
149
|
-
return z.object({
|
|
150
|
-
meetingId: z.string().min(1),
|
|
151
|
-
startTimeMs: z.number().int().positive(),
|
|
152
|
-
durationMs: z.number().int().positive(),
|
|
153
|
-
firstName: z.string().min(1, { message: "First name is required" }).max(255),
|
|
154
|
-
lastName: z.string().min(1, { message: "Last name is required" }).max(255),
|
|
155
|
-
email: z.string().email({ message: "Please enter a valid email address" }).max(255),
|
|
156
|
-
timezone: z.string().refine(isValidIanaTimezone, { message: "Invalid timezone" }),
|
|
157
|
-
locale: z.string().refine(isValidBcp47Locale, { message: "Invalid locale" }).optional(),
|
|
158
|
-
// Plain `.optional()` (no `.default()`) so zod's input and output types
|
|
159
|
-
// match — react-hook-form's zodResolver needs them identical, and the
|
|
160
|
-
// server handles `undefined` explicitly anyway. REQUIRED when the link
|
|
161
|
-
// declares required questions (see `hasRequiredAnswers` above).
|
|
162
|
-
formFields: hasRequiredAnswers ? answersObject : answersObject.optional(),
|
|
163
|
-
legalConsentResponses: z.array(z.object({ communicationTypeId: z.string(), consented: z.boolean() })).optional()
|
|
164
|
-
}).superRefine((data, ctx) => {
|
|
165
|
-
const responses = data.legalConsentResponses ?? [];
|
|
166
|
-
const ok = requiredConsentIds.every(
|
|
167
|
-
(id) => responses.some((r) => r.communicationTypeId === id && r.consented === true)
|
|
168
|
-
);
|
|
169
|
-
if (!ok) {
|
|
170
|
-
ctx.addIssue({
|
|
171
|
-
code: z.ZodIssueCode.custom,
|
|
172
|
-
path: ["legalConsentResponses"],
|
|
173
|
-
message: "Required consent checkboxes must be accepted"
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// src/hooks/use-meeting-booking.ts
|
|
180
89
|
var AVAILABILITY_STALE_MS = 0;
|
|
181
90
|
function meetingAvailabilityKey(apiBaseUrl, meetingId, monthOffset) {
|
|
182
91
|
return ["meeting-availability", apiBaseUrl, meetingId, monthOffset];
|
|
@@ -225,7 +134,7 @@ function useMeetingBooking(options) {
|
|
|
225
134
|
const data = await res.json().catch(() => ({}));
|
|
226
135
|
if (res.ok) return { ok: true, confirmation: data };
|
|
227
136
|
const rawCode = typeof data.code === "string" ? data.code : "";
|
|
228
|
-
const code =
|
|
137
|
+
const code = KNOWN_CODES.includes(rawCode) ? rawCode : "TEMPORARILY_UNAVAILABLE";
|
|
229
138
|
return { ok: false, code, message: typeof data.error === "string" ? data.error : void 0 };
|
|
230
139
|
} catch {
|
|
231
140
|
return { ok: false, code: "TEMPORARILY_UNAVAILABLE" };
|
|
@@ -264,6 +173,94 @@ function useMeetingBooking(options) {
|
|
|
264
173
|
};
|
|
265
174
|
}
|
|
266
175
|
|
|
176
|
+
// src/schemas/meeting-booking-schema.ts
|
|
177
|
+
import { z } from "zod";
|
|
178
|
+
var SUPPORTED_FORM_FIELD_TYPES = ["text", "textarea", "select", "radio", "checkbox"];
|
|
179
|
+
function isSupportedFormField(field) {
|
|
180
|
+
return SUPPORTED_FORM_FIELD_TYPES.includes(field.type);
|
|
181
|
+
}
|
|
182
|
+
var IANA_TZ_RE = /^(?:UTC|[A-Za-z_]+(?:\/[A-Za-z0-9_+-]+)+)$/;
|
|
183
|
+
function isValidIanaTimezone(tz) {
|
|
184
|
+
if (!IANA_TZ_RE.test(tz)) return false;
|
|
185
|
+
try {
|
|
186
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
187
|
+
return true;
|
|
188
|
+
} catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function isValidBcp47Locale(locale) {
|
|
193
|
+
try {
|
|
194
|
+
return Intl.getCanonicalLocales(locale).length > 0;
|
|
195
|
+
} catch {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function makeBookingSchema(formFields, legalConsent) {
|
|
200
|
+
const answers = {};
|
|
201
|
+
for (const field of formFields) {
|
|
202
|
+
if (!isSupportedFormField(field)) continue;
|
|
203
|
+
let validator;
|
|
204
|
+
switch (field.type) {
|
|
205
|
+
case "checkbox":
|
|
206
|
+
validator = z.boolean();
|
|
207
|
+
break;
|
|
208
|
+
case "select":
|
|
209
|
+
case "radio":
|
|
210
|
+
validator = z.string().refine((v) => !v || (field.options ?? []).includes(v), {
|
|
211
|
+
message: `Please choose a valid option for ${field.label}`
|
|
212
|
+
});
|
|
213
|
+
break;
|
|
214
|
+
case "textarea":
|
|
215
|
+
validator = z.string().max(5e3, { message: `${field.label} is too long` });
|
|
216
|
+
break;
|
|
217
|
+
case "text":
|
|
218
|
+
default:
|
|
219
|
+
validator = z.string().max(1e3, { message: `${field.label} is too long` });
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
if (field.required) {
|
|
223
|
+
validator = field.type === "checkbox" ? z.literal(true, { message: `${field.label} is required` }) : validator.min(1, { message: `${field.label} is required` });
|
|
224
|
+
} else if (field.type !== "checkbox") {
|
|
225
|
+
validator = validator.optional().or(z.literal(""));
|
|
226
|
+
} else {
|
|
227
|
+
validator = z.boolean().optional();
|
|
228
|
+
}
|
|
229
|
+
answers[field.name] = validator;
|
|
230
|
+
}
|
|
231
|
+
const requiredConsentIds = (legalConsent?.communicationConsentCheckboxes ?? []).filter((c) => c.required).map((c) => c.communicationTypeId);
|
|
232
|
+
const hasRequiredAnswers = formFields.some((f) => isSupportedFormField(f) && f.required);
|
|
233
|
+
const answersObject = z.object(answers);
|
|
234
|
+
return z.object({
|
|
235
|
+
meetingId: z.string().min(1),
|
|
236
|
+
startTimeMs: z.number().int().positive(),
|
|
237
|
+
durationMs: z.number().int().positive(),
|
|
238
|
+
firstName: z.string().min(1, { message: "First name is required" }).max(255),
|
|
239
|
+
lastName: z.string().min(1, { message: "Last name is required" }).max(255),
|
|
240
|
+
email: z.string().email({ message: "Please enter a valid email address" }).max(255),
|
|
241
|
+
timezone: z.string().refine(isValidIanaTimezone, { message: "Invalid timezone" }),
|
|
242
|
+
locale: z.string().refine(isValidBcp47Locale, { message: "Invalid locale" }).optional(),
|
|
243
|
+
// Plain `.optional()` (no `.default()`) so zod's input and output types
|
|
244
|
+
// match — react-hook-form's zodResolver needs them identical, and the
|
|
245
|
+
// server handles `undefined` explicitly anyway. REQUIRED when the link
|
|
246
|
+
// declares required questions (see `hasRequiredAnswers` above).
|
|
247
|
+
formFields: hasRequiredAnswers ? answersObject : answersObject.optional(),
|
|
248
|
+
legalConsentResponses: z.array(z.object({ communicationTypeId: z.string(), consented: z.boolean() })).optional()
|
|
249
|
+
}).superRefine((data, ctx) => {
|
|
250
|
+
const responses = data.legalConsentResponses ?? [];
|
|
251
|
+
const ok = requiredConsentIds.every(
|
|
252
|
+
(id) => responses.some((r) => r.communicationTypeId === id && r.consented === true)
|
|
253
|
+
);
|
|
254
|
+
if (!ok) {
|
|
255
|
+
ctx.addIssue({
|
|
256
|
+
code: z.ZodIssueCode.custom,
|
|
257
|
+
path: ["legalConsentResponses"],
|
|
258
|
+
message: "Required consent checkboxes must be accepted"
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
267
264
|
// src/components/meeting-scheduler/booking-form.tsx
|
|
268
265
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
269
266
|
import { useMemo } from "react";
|