@blazeo.com/appointment-client 1.0.3 → 1.0.5

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,545 @@
1
+ import { useMemo, useState } from "react";
2
+ import {
3
+ CalendarModel,
4
+ deleteCalendarAsync,
5
+ fetchCalendarDetails,
6
+ fetchCalendarWithOpeningHours,
7
+ updateCalendarAsync,
8
+ } from "appointment-client";
9
+ import { getSnapshot, isStateTreeNode } from "mobx-state-tree";
10
+ import { configureBlazeoFromEffective, useBlazeoConnection } from "./BlazeoConnectionSettings.jsx";
11
+ import { getExampleCalendarBOInput } from "./CreateCalendarTab.jsx";
12
+
13
+ function pick(row, ...keys) {
14
+ if (row == null || typeof row !== "object") return undefined;
15
+ for (const k of keys) {
16
+ if (row[k] !== undefined && row[k] !== null) return row[k];
17
+ }
18
+ return undefined;
19
+ }
20
+
21
+ function pad2(n) {
22
+ const v = Number(n);
23
+ if (Number.isNaN(v)) return "—";
24
+ return String(v).padStart(2, "0");
25
+ }
26
+
27
+ /**
28
+ * Browser `fetch` often surfaces blocked requests as TypeError "Failed to fetch" (e.g. CORS).
29
+ */
30
+ function explainFetchFailure(err, configuredBaseUrl) {
31
+ const msg = err instanceof Error ? err.message : String(err);
32
+ const isNetwork =
33
+ msg === "Failed to fetch" ||
34
+ msg === "Load failed" ||
35
+ (err instanceof TypeError && (/fetch/i.test(msg) || /network/i.test(msg)));
36
+ if (!isNetwork) return msg;
37
+
38
+ const isRemote =
39
+ configuredBaseUrl &&
40
+ /^https?:\/\//i.test(configuredBaseUrl) &&
41
+ !/localhost|127\.0\.0\.1/i.test(configuredBaseUrl);
42
+
43
+ const proxyHint =
44
+ "Dev workaround (Vite proxy): create sample/.env.development with\n" +
45
+ " VITE_DEV_PROXY_TARGET=https://YOUR_API_ORIGIN\n" +
46
+ "restart npm run dev, then set Base URL to:\n" +
47
+ " http://localhost:5173/blazeo-api\n" +
48
+ "Consumer header stays the same.";
49
+
50
+ if (isRemote) {
51
+ return `${msg}\n\nLikely CORS / blocked browser cross-origin request.\nFix API CORS for http://localhost:5173, OR:\n${proxyHint}`;
52
+ }
53
+ return `${msg}\n\n${proxyHint}`;
54
+ }
55
+
56
+ /** Opening hours list from participant API wrapper or embedded `calendar.openingHours`. */
57
+ function pickOpeningHoursListFromBundle(parsed) {
58
+ const fromCal = parsed?.calendar?.openingHours;
59
+ if (Array.isArray(fromCal) && fromCal.length > 0) return fromCal;
60
+ const oh = parsed?.openingHours;
61
+ if (oh == null) return null;
62
+ if (Array.isArray(oh)) return oh.length ? oh : null;
63
+ const d = oh.data ?? oh.Data ?? oh;
64
+ if (Array.isArray(d)) return d.length ? d : null;
65
+ if (d && typeof d === "object") {
66
+ if (Array.isArray(d.openingHours)) return d.openingHours;
67
+ if (Array.isArray(d.items)) return d.items;
68
+ }
69
+ return null;
70
+ }
71
+
72
+ function OpeningHoursSummary({ outputJson }) {
73
+ let list = null;
74
+ try {
75
+ const parsed = JSON.parse(outputJson);
76
+ list = pickOpeningHoursListFromBundle(parsed);
77
+ } catch {
78
+ return null;
79
+ }
80
+ if (!Array.isArray(list) || list.length === 0) {
81
+ return (
82
+ <p className="muted small" style={{ marginBottom: "0.75rem" }}>
83
+ No opening-hours rows parsed for the table. Check{" "}
84
+ <code>calendar.openingHours</code> or <code>openingHours</code> in the JSON below.
85
+ </p>
86
+ );
87
+ }
88
+
89
+ return (
90
+ <div style={{ marginBottom: "1rem" }}>
91
+ <h3 style={{ margin: "0 0 0.5rem", fontSize: "0.95rem", color: "#334155" }}>
92
+ Opening hours (quick view)
93
+ </h3>
94
+ <table className="table">
95
+ <thead>
96
+ <tr>
97
+ <th>Day(s)</th>
98
+ <th>Start</th>
99
+ <th>End</th>
100
+ <th>Off</th>
101
+ <th>Participant</th>
102
+ </tr>
103
+ </thead>
104
+ <tbody>
105
+ {list.map((row, idx) => {
106
+ const days = pick(row, "days", "Days");
107
+ const day = pick(row, "day", "Day");
108
+ const dayLabel = Array.isArray(days)
109
+ ? days.join(", ")
110
+ : day != null
111
+ ? String(day)
112
+ : "—";
113
+ const sh = pick(row, "startHour", "StartHour", "start_hour");
114
+ const sm = pick(row, "startMinute", "StartMinute", "start_minute");
115
+ const eh = pick(row, "endHour", "EndHour", "end_hour");
116
+ const em = pick(row, "endMinute", "EndMinute", "end_minute");
117
+ const off = pick(row, "off", "Off");
118
+ const pid = pick(row, "participantId", "ParticipantId", "participant_id");
119
+ const member = pick(row, "member", "Member");
120
+ const participantDisplay =
121
+ pid != null ? String(pid) : member != null ? String(member) : "—";
122
+ return (
123
+ <tr key={idx}>
124
+ <td className="table__code">{dayLabel}</td>
125
+ <td className="table__code">
126
+ {pad2(sh)}:{pad2(sm)}
127
+ </td>
128
+ <td className="table__code">
129
+ {pad2(eh)}:{pad2(em)}
130
+ </td>
131
+ <td className="table__code">{off === true || off === "true" ? "yes" : "no"}</td>
132
+ <td className="table__code">{participantDisplay}</td>
133
+ </tr>
134
+ );
135
+ })}
136
+ </tbody>
137
+ </table>
138
+ <p className="muted small" style={{ marginTop: "0.5rem" }}>
139
+ Source:{" "}
140
+ {(() => {
141
+ try {
142
+ const p = JSON.parse(outputJson);
143
+ if (p?.__openingHoursMeta?.fromCalendarGet) return "embedded on GET /Calendar/Get";
144
+ if (p?.__openingHoursMeta?.fromParticipantApi)
145
+ return "GET /Calendar/Participant/OpeningHours/Get";
146
+ return "see JSON";
147
+ } catch {
148
+ return "—";
149
+ }
150
+ })()}
151
+ </p>
152
+ </div>
153
+ );
154
+ }
155
+
156
+ function toDisplayJson(value) {
157
+ if (value == null) return JSON.stringify(value, null, 2);
158
+ if (Array.isArray(value) && value.every(isStateTreeNode)) {
159
+ return JSON.stringify(value.map((n) => getSnapshot(n)), null, 2);
160
+ }
161
+ if (isStateTreeNode(value)) return JSON.stringify(getSnapshot(value), null, 2);
162
+ try {
163
+ return JSON.stringify(value, null, 2);
164
+ } catch {
165
+ return String(value);
166
+ }
167
+ }
168
+
169
+ /** MST `Calendar` snapshot uses `id` (server id). `CalendarInput` / update mapper expect `serverId`. */
170
+ function calendarSnapshotToUpdatePayload(snap) {
171
+ const copy = { ...snap };
172
+ const serverId = copy.id;
173
+ delete copy.id;
174
+ if (serverId != null) copy.serverId = serverId;
175
+ return copy;
176
+ }
177
+
178
+ export function FetchCalendarTab() {
179
+ const { effective } = useBlazeoConnection();
180
+ const [calendarId, setCalendarId] = useState("");
181
+ const [companyKey, setCompanyKey] = useState("");
182
+ const [busy, setBusy] = useState(false);
183
+ const [note, setNote] = useState("");
184
+ const [output, setOutput] = useState("");
185
+ const [error, setError] = useState("");
186
+
187
+ const initialUpdateJson = useMemo(() => {
188
+ const ex = getExampleCalendarBOInput();
189
+ ex.calendarId = "paste-or-use-button-below";
190
+ return JSON.stringify(ex, null, 2);
191
+ }, []);
192
+ const [updateJson, setUpdateJson] = useState(initialUpdateJson);
193
+ const [mutateNote, setMutateNote] = useState("");
194
+ const [mutateOutput, setMutateOutput] = useState("");
195
+ const [lastFetchUpdatePayload, setLastFetchUpdatePayload] = useState(null);
196
+
197
+ function ensureBaseConfigured() {
198
+ if (!effective.baseUrl) {
199
+ setError("Set Base URL in the connection card above or in `blazeoClientDefaults.ts`.");
200
+ return false;
201
+ }
202
+ return true;
203
+ }
204
+
205
+ async function handleFetchCalendar(e) {
206
+ e.preventDefault();
207
+ setError("");
208
+ setNote("");
209
+ setOutput("");
210
+ setLastFetchUpdatePayload(null);
211
+ const id = calendarId.trim();
212
+ if (!id) {
213
+ setError("Enter a calendar id.");
214
+ return;
215
+ }
216
+ if (!ensureBaseConfigured()) return;
217
+ configureBlazeoFromEffective(effective);
218
+
219
+ setBusy(true);
220
+ try {
221
+ const details = await fetchCalendarDetails(id, { includeParticipantsInfo: true });
222
+
223
+ if (details.cal == null) {
224
+ const raw = await CalendarModel.getRaw(id);
225
+ setNote("CalendarModel.get returned null. Showing CalendarModel.getRaw only.");
226
+ setOutput(toDisplayJson(raw));
227
+ return;
228
+ }
229
+
230
+ const snap = getSnapshot(details.cal);
231
+ setLastFetchUpdatePayload(JSON.stringify(calendarSnapshotToUpdatePayload(snap), null, 2));
232
+
233
+ const payload = {
234
+ calendar: details.calendar,
235
+ openingHours: details.openingHours,
236
+ openingHoursApiResponse: details.openingHoursApiResponse ?? null,
237
+ participants: details.participants,
238
+ participantsInfo: details.participantsInfo ?? null,
239
+ meta: details.meta,
240
+ };
241
+
242
+ setOutput(toDisplayJson(payload));
243
+ setNote("Loaded calendar + opening hours + participants (3 calls) → single response object.");
244
+ } catch (err) {
245
+ setError(explainFetchFailure(err, effective.baseUrl));
246
+ } finally {
247
+ setBusy(false);
248
+ }
249
+ }
250
+
251
+ async function handleFetchByCompany(e) {
252
+ e.preventDefault();
253
+ setError("");
254
+ setNote("");
255
+ setOutput("");
256
+ setLastFetchUpdatePayload(null);
257
+ const key = companyKey.trim();
258
+ if (!key) {
259
+ setError("Enter a company key.");
260
+ return;
261
+ }
262
+ if (!ensureBaseConfigured()) return;
263
+ configureBlazeoFromEffective(effective);
264
+
265
+ setBusy(true);
266
+ try {
267
+ const list = await CalendarModel.getByCompany(key);
268
+ if (list == null || list.length === 0) {
269
+ setNote("getByCompany returned null or an empty list.");
270
+ setOutput(toDisplayJson(list));
271
+ } else {
272
+ const enriched = await Promise.all(
273
+ list.map(async (c) => {
274
+ const id = c.calendarId ?? String(c.id ?? "");
275
+ if (!id) return { calendar: getSnapshot(c), openingHours: [], meta: { error: "no id" } };
276
+ try {
277
+ const b = await fetchCalendarDetails(id);
278
+ return {
279
+ calendar: b.calendar ?? getSnapshot(c),
280
+ openingHours: b.openingHours,
281
+ participants: b.participants,
282
+ meta: b.meta,
283
+ };
284
+ } catch (err) {
285
+ return {
286
+ calendar: getSnapshot(c),
287
+ openingHours: [],
288
+ meta: {
289
+ error: err instanceof Error ? err.message : String(err),
290
+ },
291
+ };
292
+ }
293
+ })
294
+ );
295
+ setNote(
296
+ `Loaded ${list.length} calendar(s); opening hours + participants loaded per calendar (single details bundle).`
297
+ );
298
+ setOutput(toDisplayJson(enriched));
299
+ }
300
+ } catch (err) {
301
+ setError(explainFetchFailure(err, effective.baseUrl));
302
+ } finally {
303
+ setBusy(false);
304
+ }
305
+ }
306
+
307
+ function handleFillUpdateFromLastFetch() {
308
+ if (lastFetchUpdatePayload == null) {
309
+ setError("Fetch one calendar first (successful CalendarModel.get), then use this button.");
310
+ return;
311
+ }
312
+ setUpdateJson(lastFetchUpdatePayload);
313
+ setError("");
314
+ setMutateNote("");
315
+ setMutateOutput("");
316
+ }
317
+
318
+ function handleInjectCalendarIdIntoUpdateJson() {
319
+ const id = calendarId.trim();
320
+ if (!id) {
321
+ setError("Enter a calendar id above first, or edit the JSON manually.");
322
+ return;
323
+ }
324
+ try {
325
+ const obj = JSON.parse(updateJson);
326
+ obj.calendarId = id;
327
+ setUpdateJson(JSON.stringify(obj, null, 2));
328
+ setError("");
329
+ } catch (err) {
330
+ setError(err instanceof Error ? err.message : "Update JSON is not valid JSON.");
331
+ }
332
+ }
333
+
334
+ async function handleUpdateCalendar(e) {
335
+ e.preventDefault();
336
+ setError("");
337
+ setMutateNote("");
338
+ setMutateOutput("");
339
+ if (!ensureBaseConfigured()) return;
340
+ configureBlazeoFromEffective(effective);
341
+ let payload;
342
+ try {
343
+ payload = JSON.parse(updateJson);
344
+ } catch (err) {
345
+ setError(`Invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
346
+ return;
347
+ }
348
+ setBusy(true);
349
+ try {
350
+ const result = await updateCalendarAsync(payload, {});
351
+ if (result.ok) {
352
+ setMutateNote("updateCalendarAsync → POST /Calendar/Event/Update");
353
+ setMutateOutput(
354
+ JSON.stringify(
355
+ {
356
+ snapshot: getSnapshot(result.calendar),
357
+ apiResponse: result.apiResponse ?? null,
358
+ },
359
+ null,
360
+ 2
361
+ )
362
+ );
363
+ } else {
364
+ setError(result.error);
365
+ if (result.apiResponse != null) {
366
+ setMutateOutput(JSON.stringify(result.apiResponse, null, 2));
367
+ }
368
+ }
369
+ } finally {
370
+ setBusy(false);
371
+ }
372
+ }
373
+
374
+ async function handleDeleteCalendar(e) {
375
+ e.preventDefault();
376
+ setError("");
377
+ setMutateNote("");
378
+ setMutateOutput("");
379
+ const id = calendarId.trim();
380
+ if (!id) {
381
+ setError("Enter a calendar id to delete.");
382
+ return;
383
+ }
384
+ if (!ensureBaseConfigured()) return;
385
+ configureBlazeoFromEffective(effective);
386
+ if (
387
+ !window.confirm(
388
+ `Delete calendar "${id}"?\n\nThis calls GET /Calendar/Remove (cannot be undone on the server).`
389
+ )
390
+ ) {
391
+ return;
392
+ }
393
+ setBusy(true);
394
+ try {
395
+ const result = await deleteCalendarAsync(id, {});
396
+ if (result.ok) {
397
+ setMutateNote("deleteCalendarAsync → GET /Calendar/Remove");
398
+ setMutateOutput(JSON.stringify({ calendarId: id, apiResponse: result.apiResponse ?? null }, null, 2));
399
+ } else {
400
+ setError(result.error);
401
+ if (result.apiResponse != null) {
402
+ setMutateOutput(JSON.stringify(result.apiResponse, null, 2));
403
+ }
404
+ }
405
+ } finally {
406
+ setBusy(false);
407
+ }
408
+ }
409
+
410
+ return (
411
+ <>
412
+ <div className="card">
413
+ <h2>Fetch calendar</h2>
414
+ <p className="muted small">
415
+ Uses <code>fetchCalendarWithOpeningHours</code> from <code>appointment-client</code>: merges
416
+ embedded <code>openingHours</code> from <code>GET /Calendar/Get</code> when present (MST omits
417
+ them); otherwise calls <code>calendar.getParticipantOpeningHours()</code> (
418
+ <code>GET /Calendar/Participant/OpeningHours/Get</code>), matching{" "}
419
+ <code>@blazeo.com/calendar-client</code>.
420
+ </p>
421
+ <p className="muted small">
422
+ Effective: <code>{effective.baseUrl || "(set connection or blazeoClientDefaults.ts)"}</code>
423
+ {effective.consumer ? (
424
+ <>
425
+ {" "}
426
+ · Consumer: <code>{effective.consumer}</code>
427
+ </>
428
+ ) : null}
429
+ </p>
430
+
431
+ <form onSubmit={handleFetchCalendar} className="form">
432
+ <label className="form__label">
433
+ <span>Calendar id</span>
434
+ <input
435
+ type="text"
436
+ className="form__input"
437
+ placeholder="Calendar / third-party id"
438
+ value={calendarId}
439
+ onChange={(e) => setCalendarId(e.target.value)}
440
+ autoComplete="off"
441
+ />
442
+ </label>
443
+ <button type="submit" className="btn btn--primary" disabled={busy}>
444
+ {busy ? "Loading…" : "Fetch calendar + opening hours"}
445
+ </button>
446
+ </form>
447
+ </div>
448
+
449
+ <div className="card">
450
+ <h2>Update calendar</h2>
451
+ <div className="form-actions form-actions--top">
452
+ <button
453
+ type="button"
454
+ className="btn btn--secondary"
455
+ disabled={busy || lastFetchUpdatePayload == null}
456
+ onClick={handleFillUpdateFromLastFetch}
457
+ >
458
+ Fill update form from last fetch
459
+ </button>
460
+ <button type="button" className="btn btn--secondary" disabled={busy} onClick={handleInjectCalendarIdIntoUpdateJson}>
461
+ Set calendarId from field above
462
+ </button>
463
+ </div>
464
+ <form onSubmit={handleUpdateCalendar} className="form">
465
+ <label className="form__label">
466
+ <span>Payload (JSON)</span>
467
+ <textarea
468
+ className="form__textarea"
469
+ value={updateJson}
470
+ onChange={(e) => setUpdateJson(e.target.value)}
471
+ spellCheck={false}
472
+ rows={14}
473
+ aria-label="Calendar update JSON"
474
+ />
475
+ </label>
476
+ <button type="submit" className="btn btn--primary" disabled={busy}>
477
+ {busy ? "Working…" : "Update calendar"}
478
+ </button>
479
+ </form>
480
+ </div>
481
+
482
+ <div className="card">
483
+ <h2>Delete calendar</h2>
484
+ <form onSubmit={handleDeleteCalendar} className="form">
485
+ <button type="submit" className="btn btn--secondary" disabled={busy}>
486
+ {busy ? "Working…" : "Delete calendar"}
487
+ </button>
488
+ </form>
489
+ </div>
490
+
491
+ <div className="card">
492
+ <h2>Fetch calendars by company</h2>
493
+ <p className="muted small">
494
+ Calls <code>CalendarModel.getByCompany</code> → <code>GET /Calendar/All</code>. If the UI shows{" "}
495
+ <strong>Failed to fetch</strong> while Base URL points at Azure/production, that is usually{" "}
496
+ <strong>CORS</strong>: enable proxy via <code>VITE_DEV_PROXY_TARGET</code> in{" "}
497
+ <code>sample/.env.development</code> and Base URL <code>http://localhost:5173/blazeo-api</code>{" "}
498
+ (restart dev server).
499
+ </p>
500
+ <form onSubmit={handleFetchByCompany} className="form">
501
+ <label className="form__label">
502
+ <span>Company key</span>
503
+ <input
504
+ type="text"
505
+ className="form__input"
506
+ placeholder="company_key"
507
+ value={companyKey}
508
+ onChange={(e) => setCompanyKey(e.target.value)}
509
+ autoComplete="off"
510
+ />
511
+ </label>
512
+ <button type="submit" className="btn btn--secondary" disabled={busy}>
513
+ {busy ? "Loading…" : "Fetch calendars"}
514
+ </button>
515
+ </form>
516
+ </div>
517
+
518
+ {error ? (
519
+ <div className="card card--error" role="alert">
520
+ <h2>Error</h2>
521
+ <pre className="pre-block">{error}</pre>
522
+ </div>
523
+ ) : null}
524
+
525
+ {note ? <p className="muted small">{note}</p> : null}
526
+
527
+ {output ? (
528
+ <div className="card card--success">
529
+ <h2>Calendar + opening hours &amp; participants</h2>
530
+ <OpeningHoursSummary outputJson={output} />
531
+ <pre className="pre-block">{output}</pre>
532
+ </div>
533
+ ) : null}
534
+
535
+ {mutateNote ? <p className="muted small">{mutateNote}</p> : null}
536
+
537
+ {mutateOutput ? (
538
+ <div className="card card--success">
539
+ <h2>Update / delete result</h2>
540
+ <pre className="pre-block">{mutateOutput}</pre>
541
+ </div>
542
+ ) : null}
543
+ </>
544
+ );
545
+ }
@@ -0,0 +1,102 @@
1
+ import { useMemo, useState } from "react";
2
+ import { getExampleParticipants, ParticipantModel } from "appointment-client";
3
+ import { getSnapshot, isStateTreeNode } from "mobx-state-tree";
4
+ import {
5
+ configureBlazeoFromEffective,
6
+ useBlazeoConnection,
7
+ } from "./BlazeoConnectionSettings.jsx";
8
+
9
+ function toDisplayJson(value) {
10
+ if (value == null) return JSON.stringify(value, null, 2);
11
+ if (Array.isArray(value) && value.every(isStateTreeNode)) {
12
+ return JSON.stringify(value.map((n) => getSnapshot(n)), null, 2);
13
+ }
14
+ if (isStateTreeNode(value)) return JSON.stringify(getSnapshot(value), null, 2);
15
+ try {
16
+ return JSON.stringify(value, null, 2);
17
+ } catch {
18
+ return String(value);
19
+ }
20
+ }
21
+
22
+ export function ParticipantTab() {
23
+ const { effective } = useBlazeoConnection();
24
+ const example = useMemo(() => getExampleParticipants(), []);
25
+ const [calendarId, setCalendarId] = useState("");
26
+ const [busy, setBusy] = useState(false);
27
+ const [error, setError] = useState("");
28
+ const [output, setOutput] = useState("");
29
+
30
+ async function handleFetchParticipants(e) {
31
+ e.preventDefault();
32
+ setError("");
33
+ setOutput("");
34
+ const id = calendarId.trim();
35
+ if (!id) {
36
+ setError("Enter a calendar id.");
37
+ return;
38
+ }
39
+ if (!effective.baseUrl) {
40
+ setError("Set Base URL in the connection card above.");
41
+ return;
42
+ }
43
+ configureBlazeoFromEffective(effective);
44
+ setBusy(true);
45
+ try {
46
+ const res = await ParticipantModel.getAllByCalendar(id);
47
+ setOutput(toDisplayJson(res));
48
+ } catch (err) {
49
+ setError(err instanceof Error ? err.message : String(err));
50
+ } finally {
51
+ setBusy(false);
52
+ }
53
+ }
54
+
55
+ return (
56
+ <>
57
+ <div className="card">
58
+ <h2>Participants</h2>
59
+ <p className="muted small">
60
+ Example participants from <code>getExampleParticipants()</code> plus a fetch helper.
61
+ </p>
62
+ <pre className="pre-block">{JSON.stringify(example, null, 2)}</pre>
63
+ </div>
64
+
65
+ <div className="card">
66
+ <h2>Fetch participants by calendar</h2>
67
+ <p className="muted small">
68
+ Calls <code>ParticipantModel.getAllByCalendar(calendarId)</code>.
69
+ </p>
70
+ <form onSubmit={handleFetchParticipants} className="form">
71
+ <label className="form__label">
72
+ <span>Calendar id</span>
73
+ <input
74
+ type="text"
75
+ className="form__input"
76
+ value={calendarId}
77
+ onChange={(e) => setCalendarId(e.target.value)}
78
+ />
79
+ </label>
80
+ <button type="submit" className="btn btn--secondary" disabled={busy}>
81
+ {busy ? "Loading…" : "Fetch participants"}
82
+ </button>
83
+ </form>
84
+ </div>
85
+
86
+ {error ? (
87
+ <div className="card card--error" role="alert">
88
+ <h2>Error</h2>
89
+ <pre className="pre-block">{error}</pre>
90
+ </div>
91
+ ) : null}
92
+
93
+ {output ? (
94
+ <div className="card card--success">
95
+ <h2>Result</h2>
96
+ <pre className="pre-block">{output}</pre>
97
+ </div>
98
+ ) : null}
99
+ </>
100
+ );
101
+ }
102
+
@@ -0,0 +1,19 @@
1
+ import { applyBlazeoClientConfig } from "appointment-client";
2
+ import { StrictMode } from "react";
3
+ import { createRoot } from "react-dom/client";
4
+ import { App } from "./App2.jsx";
5
+ import "./style.css";
6
+
7
+ /** Apply file defaults (`blazeoClientDefaults.ts`) before React mounts. */
8
+ applyBlazeoClientConfig();
9
+
10
+ const rootEl = document.getElementById("app");
11
+ if (!rootEl) {
12
+ throw new Error("Missing #app element");
13
+ }
14
+
15
+ createRoot(rootEl).render(
16
+ <StrictMode>
17
+ <App />
18
+ </StrictMode>
19
+ );