@blazeo.com/appointment-client 1.0.3 → 1.0.4
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/blazeo.com-appointment-client-1.0.4.tgz +0 -0
- package/dist/calendar/fetchCalendarWithOpeningHours.d.ts +21 -0
- package/dist/calendar/fetchCalendarWithOpeningHours.js +75 -0
- package/dist/config/initializeAppointmentClient.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +23 -39
- package/sample/index.html +13 -0
- package/sample/package-lock.json +1658 -0
- package/sample/package.json +19 -0
- package/sample/src/App2.jsx +148 -0
- package/sample/src/AvailabilityTab.jsx +83 -0
- package/sample/src/BlazeoConnectionSettings.jsx +118 -0
- package/sample/src/CalendarTab.jsx +37 -0
- package/sample/src/CreateCalendarTab.jsx +147 -0
- package/sample/src/EventTab.jsx +244 -0
- package/sample/src/FetchCalendarTab.jsx +566 -0
- package/sample/src/ParticipantTab.jsx +102 -0
- package/sample/src/main.jsx +19 -0
- package/sample/src/style.css +681 -0
- package/sample/src/vite-env.d.ts +12 -0
- package/sample/vite.config.js +47 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "appointment-client-sample",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite",
|
|
7
|
+
"build": "vite build",
|
|
8
|
+
"preview": "vite preview"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"appointment-client": "file:..",
|
|
12
|
+
"react": "^19.0.0",
|
|
13
|
+
"react-dom": "^19.0.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@vitejs/plugin-react": "^4.4.0",
|
|
17
|
+
"vite": "^6.0.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
BlazeoConnectionProvider,
|
|
4
|
+
useBlazeoConnection,
|
|
5
|
+
} from "./BlazeoConnectionSettings.jsx";
|
|
6
|
+
import { CalendarTab } from "./CalendarTab.jsx";
|
|
7
|
+
import { EventTab } from "./EventTab.jsx";
|
|
8
|
+
import { ParticipantTab } from "./ParticipantTab.jsx";
|
|
9
|
+
import { AvailabilityTab } from "./AvailabilityTab.jsx";
|
|
10
|
+
import { CreateCalendarTab } from "./CreateCalendarTab.jsx";
|
|
11
|
+
import { FetchCalendarTab } from "./FetchCalendarTab.jsx";
|
|
12
|
+
|
|
13
|
+
const TABS = [
|
|
14
|
+
{ id: "calendar", label: "Calendar" },
|
|
15
|
+
{ id: "fetch", label: "Fetch calendar" },
|
|
16
|
+
{ id: "create", label: "Create calendar" },
|
|
17
|
+
{ id: "event", label: "Event" },
|
|
18
|
+
{ id: "participant", label: "Participant" },
|
|
19
|
+
{ id: "availability", label: "Availability / booking" },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function ConnectionSettingsCard() {
|
|
23
|
+
const {
|
|
24
|
+
baseUrlInput,
|
|
25
|
+
consumerInput,
|
|
26
|
+
setBaseUrlInput,
|
|
27
|
+
setConsumerInput,
|
|
28
|
+
effective,
|
|
29
|
+
} = useBlazeoConnection();
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="card connection-card">
|
|
33
|
+
<h2>Blazeo connection</h2>
|
|
34
|
+
<p className="muted small">
|
|
35
|
+
Values are saved in <code>localStorage</code>. Empty fields fall back to{" "}
|
|
36
|
+
<code>appointment-client/src/config/blazeoClientDefaults.ts</code>.
|
|
37
|
+
</p>
|
|
38
|
+
<p className="muted small">
|
|
39
|
+
Effective:{" "}
|
|
40
|
+
<code>{effective.baseUrl || "(set below or in blazeoClientDefaults.ts)"}</code>
|
|
41
|
+
{effective.consumer ? (
|
|
42
|
+
<>
|
|
43
|
+
{" "}
|
|
44
|
+
· Consumer: <code>{effective.consumer}</code>
|
|
45
|
+
</>
|
|
46
|
+
) : null}
|
|
47
|
+
</p>
|
|
48
|
+
<div className="connection-card__row">
|
|
49
|
+
<label className="form__label">
|
|
50
|
+
<span>Base URL</span>
|
|
51
|
+
<input
|
|
52
|
+
type="url"
|
|
53
|
+
className="form__input"
|
|
54
|
+
placeholder="https://api.example.com"
|
|
55
|
+
value={baseUrlInput}
|
|
56
|
+
onChange={(e) => setBaseUrlInput(e.target.value)}
|
|
57
|
+
autoComplete="off"
|
|
58
|
+
/>
|
|
59
|
+
</label>
|
|
60
|
+
<label className="form__label">
|
|
61
|
+
<span>Consumer</span>
|
|
62
|
+
<input
|
|
63
|
+
type="text"
|
|
64
|
+
className="form__input"
|
|
65
|
+
placeholder="Optional — same as backend header"
|
|
66
|
+
value={consumerInput}
|
|
67
|
+
onChange={(e) => setConsumerInput(e.target.value)}
|
|
68
|
+
autoComplete="off"
|
|
69
|
+
/>
|
|
70
|
+
</label>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function AppShell() {
|
|
77
|
+
const [activeId, setActiveId] = useState("calendar");
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<main className="page">
|
|
81
|
+
<header className="header">
|
|
82
|
+
<h1>appointment-client</h1>
|
|
83
|
+
<p className="muted">Browser sample — tabbed explorer for the npm package</p>
|
|
84
|
+
</header>
|
|
85
|
+
|
|
86
|
+
<ConnectionSettingsCard />
|
|
87
|
+
|
|
88
|
+
<div className="tabs" role="tablist" aria-label="Sample areas">
|
|
89
|
+
{TABS.map((tab) => (
|
|
90
|
+
<button
|
|
91
|
+
key={tab.id}
|
|
92
|
+
type="button"
|
|
93
|
+
role="tab"
|
|
94
|
+
id={`tab-${tab.id}`}
|
|
95
|
+
aria-selected={activeId === tab.id}
|
|
96
|
+
aria-controls={`panel-${tab.id}`}
|
|
97
|
+
className={`tabs__btn ${activeId === tab.id ? "tabs__btn--active" : ""}`}
|
|
98
|
+
onClick={() => setActiveId(tab.id)}
|
|
99
|
+
>
|
|
100
|
+
{tab.label}
|
|
101
|
+
</button>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div className="tab-panel">
|
|
106
|
+
{activeId === "calendar" && (
|
|
107
|
+
<section role="tabpanel" id="panel-calendar" aria-labelledby="tab-calendar">
|
|
108
|
+
<CalendarTab />
|
|
109
|
+
</section>
|
|
110
|
+
)}
|
|
111
|
+
{activeId === "fetch" && (
|
|
112
|
+
<section role="tabpanel" id="panel-fetch" aria-labelledby="tab-fetch">
|
|
113
|
+
<FetchCalendarTab />
|
|
114
|
+
</section>
|
|
115
|
+
)}
|
|
116
|
+
{activeId === "create" && (
|
|
117
|
+
<section role="tabpanel" id="panel-create" aria-labelledby="tab-create">
|
|
118
|
+
<CreateCalendarTab />
|
|
119
|
+
</section>
|
|
120
|
+
)}
|
|
121
|
+
{activeId === "event" && (
|
|
122
|
+
<section role="tabpanel" id="panel-event" aria-labelledby="tab-event">
|
|
123
|
+
<EventTab />
|
|
124
|
+
</section>
|
|
125
|
+
)}
|
|
126
|
+
{activeId === "participant" && (
|
|
127
|
+
<section role="tabpanel" id="panel-participant" aria-labelledby="tab-participant">
|
|
128
|
+
<ParticipantTab />
|
|
129
|
+
</section>
|
|
130
|
+
)}
|
|
131
|
+
{activeId === "availability" && (
|
|
132
|
+
<section role="tabpanel" id="panel-availability" aria-labelledby="tab-availability">
|
|
133
|
+
<AvailabilityTab />
|
|
134
|
+
</section>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
</main>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function App() {
|
|
142
|
+
return (
|
|
143
|
+
<BlazeoConnectionProvider>
|
|
144
|
+
<AppShell />
|
|
145
|
+
</BlazeoConnectionProvider>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import { EventModel } from "appointment-client";
|
|
3
|
+
import { configureBlazeoFromEffective, useBlazeoConnection } from "./BlazeoConnectionSettings.jsx";
|
|
4
|
+
|
|
5
|
+
function parseYmd(ymd) {
|
|
6
|
+
const t = (ymd ?? "").trim();
|
|
7
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(t)) return null;
|
|
8
|
+
const [y, m, d] = t.split("-").map((x) => Number(x));
|
|
9
|
+
return { y, m, d };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function AvailabilityTab() {
|
|
13
|
+
const { effective } = useBlazeoConnection();
|
|
14
|
+
const [calendarId, setCalendarId] = useState("");
|
|
15
|
+
const [date, setDate] = useState(() => new Date().toISOString().slice(0, 10));
|
|
16
|
+
const [offsetMinutes, setOffsetMinutes] = useState(-new Date().getTimezoneOffset());
|
|
17
|
+
const [busy, setBusy] = useState(false);
|
|
18
|
+
const [error, setError] = useState("");
|
|
19
|
+
const [output, setOutput] = useState("");
|
|
20
|
+
|
|
21
|
+
const opts = useMemo(() => ({ offset: Number(offsetMinutes) || 0 }), [offsetMinutes]);
|
|
22
|
+
|
|
23
|
+
async function handleSlots(e) {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
setError("");
|
|
26
|
+
setOutput("");
|
|
27
|
+
const id = calendarId.trim();
|
|
28
|
+
const parts = parseYmd(date);
|
|
29
|
+
if (!id) return setError("Enter calendar id.");
|
|
30
|
+
if (!parts) return setError("Pick a valid date.");
|
|
31
|
+
if (!effective.baseUrl) return setError("Set Base URL above.");
|
|
32
|
+
configureBlazeoFromEffective(effective);
|
|
33
|
+
|
|
34
|
+
setBusy(true);
|
|
35
|
+
try {
|
|
36
|
+
const list = await EventModel.getAvailability(id, parts.y, parts.m, parts.d, opts);
|
|
37
|
+
setOutput(JSON.stringify(list.map((n) => n.toJSON?.() ?? n), null, 2));
|
|
38
|
+
} catch (err) {
|
|
39
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
40
|
+
} finally {
|
|
41
|
+
setBusy(false);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
<div className="card">
|
|
48
|
+
<h2>Availability</h2>
|
|
49
|
+
<form onSubmit={handleSlots} className="form">
|
|
50
|
+
<label className="form__label">
|
|
51
|
+
<span>Calendar id</span>
|
|
52
|
+
<input className="form__input" value={calendarId} onChange={(e) => setCalendarId(e.target.value)} />
|
|
53
|
+
</label>
|
|
54
|
+
<label className="form__label">
|
|
55
|
+
<span>Date</span>
|
|
56
|
+
<input type="date" className="form__input" value={date} onChange={(e) => setDate(e.target.value)} />
|
|
57
|
+
</label>
|
|
58
|
+
<label className="form__label">
|
|
59
|
+
<span>Offset minutes</span>
|
|
60
|
+
<input type="number" className="form__input" value={offsetMinutes} onChange={(e) => setOffsetMinutes(e.target.value)} />
|
|
61
|
+
</label>
|
|
62
|
+
<button className="btn btn--secondary" disabled={busy}>
|
|
63
|
+
{busy ? "Loading…" : "Load slots"}
|
|
64
|
+
</button>
|
|
65
|
+
</form>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{error ? (
|
|
69
|
+
<div className="card card--error" role="alert">
|
|
70
|
+
<h2>Error</h2>
|
|
71
|
+
<pre className="pre-block">{error}</pre>
|
|
72
|
+
</div>
|
|
73
|
+
) : null}
|
|
74
|
+
{output ? (
|
|
75
|
+
<div className="card card--success">
|
|
76
|
+
<h2>Result</h2>
|
|
77
|
+
<pre className="pre-block">{output}</pre>
|
|
78
|
+
</div>
|
|
79
|
+
) : null}
|
|
80
|
+
</>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useEffect,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { blazeoClientConfig, initializeAppointmentClient } from "appointment-client";
|
|
10
|
+
import { configure as configureCalendarClient } from "@blazeo.com/calendar-client";
|
|
11
|
+
|
|
12
|
+
const STORAGE_BASE = "appointment-client-sample:blazeoBaseUrl";
|
|
13
|
+
const STORAGE_CONSUMER = "appointment-client-sample:blazeoConsumer";
|
|
14
|
+
|
|
15
|
+
function readStored(key) {
|
|
16
|
+
try {
|
|
17
|
+
return localStorage.getItem(key) ?? "";
|
|
18
|
+
} catch {
|
|
19
|
+
return "";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function writeStored(key, value) {
|
|
24
|
+
try {
|
|
25
|
+
if (value) localStorage.setItem(key, value);
|
|
26
|
+
else localStorage.removeItem(key);
|
|
27
|
+
} catch {
|
|
28
|
+
/* ignore */
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeBase(u) {
|
|
33
|
+
const t = (u ?? "").trim();
|
|
34
|
+
if (!t) return "";
|
|
35
|
+
return t.replace(/\/+$/, "");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Merge UI fields with packaged defaults (`blazeoClientDefaults`) when inputs are empty. */
|
|
39
|
+
export function mergeBlazeoUiWithFile(uiBaseUrl, uiConsumer) {
|
|
40
|
+
const fileBase = normalizeBase(blazeoClientConfig.baseUrl ?? "");
|
|
41
|
+
const fileConsumer = (blazeoClientConfig.consumer ?? "").trim();
|
|
42
|
+
const baseUrl = normalizeBase(uiBaseUrl) || fileBase || undefined;
|
|
43
|
+
const consumer = (uiConsumer ?? "").trim() || fileConsumer || undefined;
|
|
44
|
+
return { baseUrl, consumer };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Re-apply global Blazeo `configure` from the merged connection card state.
|
|
49
|
+
* Call at the start of any handler that uses `CalendarModel` / `EventModel` HTTP so
|
|
50
|
+
* `getParticipantOpeningHours` (instance env) and static helpers always see the same
|
|
51
|
+
* `baseUrl`.
|
|
52
|
+
*/
|
|
53
|
+
export function configureBlazeoFromEffective(effective) {
|
|
54
|
+
const baseUrl = normalizeBase(effective?.baseUrl ?? "");
|
|
55
|
+
if (!baseUrl) return;
|
|
56
|
+
const consumer = (effective?.consumer ?? "").trim() || undefined;
|
|
57
|
+
// Configure appointment-client (which configures its internal calendar-client instance)
|
|
58
|
+
initializeAppointmentClient({ baseUrl, ...(consumer ? { consumer } : {}) });
|
|
59
|
+
// Also configure the calendar-client instance that the sample (and Vite alias) may resolve.
|
|
60
|
+
configureCalendarClient({ baseUrl, ...(consumer ? { consumer } : {}) });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const BlazeoConnectionContext = createContext(null);
|
|
64
|
+
|
|
65
|
+
export function BlazeoConnectionProvider({ children }) {
|
|
66
|
+
const [baseUrlInput, setBaseUrlInput] = useState(() => readStored(STORAGE_BASE));
|
|
67
|
+
const [consumerInput, setConsumerInput] = useState(() => readStored(STORAGE_CONSUMER));
|
|
68
|
+
|
|
69
|
+
const effective = useMemo(
|
|
70
|
+
() => mergeBlazeoUiWithFile(baseUrlInput, consumerInput),
|
|
71
|
+
[baseUrlInput, consumerInput]
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
writeStored(STORAGE_BASE, baseUrlInput.trim());
|
|
76
|
+
}, [baseUrlInput]);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
writeStored(STORAGE_CONSUMER, consumerInput.trim());
|
|
80
|
+
}, [consumerInput]);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Sync global Blazeo `configure` before paint so `CalendarModel.get` /
|
|
84
|
+
* `EventModel.*` static calls never see an empty `getConfig()` after the user
|
|
85
|
+
* has set Base URL (including values restored from localStorage on first paint).
|
|
86
|
+
*/
|
|
87
|
+
useLayoutEffect(() => {
|
|
88
|
+
const { baseUrl, consumer } = mergeBlazeoUiWithFile(baseUrlInput, consumerInput);
|
|
89
|
+
if (!baseUrl) return;
|
|
90
|
+
initializeAppointmentClient({ baseUrl, ...(consumer ? { consumer } : {}) });
|
|
91
|
+
configureCalendarClient({ baseUrl, ...(consumer ? { consumer } : {}) });
|
|
92
|
+
}, [baseUrlInput, consumerInput]);
|
|
93
|
+
|
|
94
|
+
const value = useMemo(
|
|
95
|
+
() => ({
|
|
96
|
+
baseUrlInput,
|
|
97
|
+
consumerInput,
|
|
98
|
+
setBaseUrlInput,
|
|
99
|
+
setConsumerInput,
|
|
100
|
+
effective,
|
|
101
|
+
}),
|
|
102
|
+
[baseUrlInput, consumerInput, effective]
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<BlazeoConnectionContext.Provider value={value}>
|
|
107
|
+
{children}
|
|
108
|
+
</BlazeoConnectionContext.Provider>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function useBlazeoConnection() {
|
|
113
|
+
const ctx = useContext(BlazeoConnectionContext);
|
|
114
|
+
if (ctx == null) {
|
|
115
|
+
throw new Error("useBlazeoConnection must be used within BlazeoConnectionProvider");
|
|
116
|
+
}
|
|
117
|
+
return ctx;
|
|
118
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CalendarClient, getExampleSlots, packageName } from "appointment-client";
|
|
2
|
+
|
|
3
|
+
const client = new CalendarClient();
|
|
4
|
+
|
|
5
|
+
export function CalendarTab() {
|
|
6
|
+
const slots = getExampleSlots();
|
|
7
|
+
const fromClient = client.getExampleSlots();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<div className="card">
|
|
12
|
+
<h2>Package</h2>
|
|
13
|
+
<p>
|
|
14
|
+
<code>packageName</code> → <strong>{packageName}</strong>
|
|
15
|
+
</p>
|
|
16
|
+
<p>
|
|
17
|
+
<code>CalendarClient#name</code> → <strong>{client.name}</strong>
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div className="card">
|
|
22
|
+
<h2>
|
|
23
|
+
<code>getExampleSlots()</code>
|
|
24
|
+
</h2>
|
|
25
|
+
<pre className="pre-block">{JSON.stringify(slots, null, 2)}</pre>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div className="card">
|
|
29
|
+
<h2>
|
|
30
|
+
<code>CalendarClient</code> instance
|
|
31
|
+
</h2>
|
|
32
|
+
<pre className="pre-block">{JSON.stringify(fromClient, null, 2)}</pre>
|
|
33
|
+
</div>
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import { createCalendarAsync, createCalendarWithRelationsAsync } from "appointment-client";
|
|
3
|
+
import { getSnapshot } from "mobx-state-tree";
|
|
4
|
+
import { configureBlazeoFromEffective, useBlazeoConnection } from "./BlazeoConnectionSettings.jsx";
|
|
5
|
+
|
|
6
|
+
/** Demo payload aligned with `CalendarBOInput` / server `CalendarBO`. */
|
|
7
|
+
export function getExampleCalendarBOInput() {
|
|
8
|
+
return {
|
|
9
|
+
companyKey: "company_key",
|
|
10
|
+
name: "Demo calendar",
|
|
11
|
+
timeZoneId: "Pakistan Standard Time",
|
|
12
|
+
assignmentMethod: 1,
|
|
13
|
+
duration: 20,
|
|
14
|
+
durationUnit: 1,
|
|
15
|
+
minimumBookingNotice: 1,
|
|
16
|
+
minimumBookingNoticeUnit: 2,
|
|
17
|
+
minimumCancelationNotice: 1,
|
|
18
|
+
minimumCancelationNoticeUnit: 2,
|
|
19
|
+
futureLimit: 1,
|
|
20
|
+
futureLimitUnit: 6,
|
|
21
|
+
bufferTime: 10,
|
|
22
|
+
bufferTimeUnit: 1,
|
|
23
|
+
bookingLimit: -1,
|
|
24
|
+
members: [{ id: "00000000-0000-0000-0000-000000000000" }],
|
|
25
|
+
openingHours: [
|
|
26
|
+
{
|
|
27
|
+
id: 1,
|
|
28
|
+
days: [1, 2, 3, 4, 5],
|
|
29
|
+
startHour: 9,
|
|
30
|
+
startMinute: 0,
|
|
31
|
+
endHour: 17,
|
|
32
|
+
endMinute: 0,
|
|
33
|
+
off: false,
|
|
34
|
+
participantId: "00000000-0000-0000-0000-000000000000",
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function CreateCalendarTab() {
|
|
41
|
+
const { effective } = useBlazeoConnection();
|
|
42
|
+
const [localOnly, setLocalOnly] = useState(false);
|
|
43
|
+
const [saveRelations, setSaveRelations] = useState(true);
|
|
44
|
+
const [busy, setBusy] = useState(false);
|
|
45
|
+
const [error, setError] = useState("");
|
|
46
|
+
const [output, setOutput] = useState("");
|
|
47
|
+
|
|
48
|
+
const [jsonText, setJsonText] = useState(() =>
|
|
49
|
+
JSON.stringify(getExampleCalendarBOInput(), null, 2)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const hint = useMemo(() => {
|
|
53
|
+
if (localOnly) return "Local only: no HTTP.";
|
|
54
|
+
if (!effective.baseUrl) return "Set Base URL above first.";
|
|
55
|
+
return saveRelations ? "Will save calendar + participants + opening hours." : "Will save calendar body only.";
|
|
56
|
+
}, [localOnly, effective.baseUrl, saveRelations]);
|
|
57
|
+
|
|
58
|
+
async function handleSubmit(e) {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
setError("");
|
|
61
|
+
setOutput("");
|
|
62
|
+
let payload;
|
|
63
|
+
try {
|
|
64
|
+
payload = JSON.parse(jsonText);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
setError(`Invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (!localOnly && !effective.baseUrl) {
|
|
70
|
+
setError("Set Base URL in the connection card above.");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!localOnly) configureBlazeoFromEffective(effective);
|
|
74
|
+
|
|
75
|
+
const hasRelations = (payload.members?.length ?? 0) > 0 || (payload.openingHours?.length ?? 0) > 0;
|
|
76
|
+
const useRelations = saveRelations && hasRelations && !localOnly;
|
|
77
|
+
|
|
78
|
+
setBusy(true);
|
|
79
|
+
try {
|
|
80
|
+
const result = useRelations
|
|
81
|
+
? await createCalendarWithRelationsAsync(payload, { localOnly })
|
|
82
|
+
: await createCalendarAsync(payload, { localOnly });
|
|
83
|
+
setOutput(JSON.stringify(result.ok ? getSnapshot(result.calendar) : result, null, 2));
|
|
84
|
+
if (!result.ok) setError(result.error);
|
|
85
|
+
} finally {
|
|
86
|
+
setBusy(false);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<>
|
|
92
|
+
<div className="card">
|
|
93
|
+
<h2>Create calendar</h2>
|
|
94
|
+
<p className="muted small">{hint}</p>
|
|
95
|
+
<label className="form__label">
|
|
96
|
+
<span>
|
|
97
|
+
<input type="checkbox" checked={localOnly} onChange={(e) => setLocalOnly(e.target.checked)} /> Local only
|
|
98
|
+
</span>
|
|
99
|
+
</label>
|
|
100
|
+
<label className="form__label">
|
|
101
|
+
<span>
|
|
102
|
+
<input
|
|
103
|
+
type="checkbox"
|
|
104
|
+
checked={saveRelations}
|
|
105
|
+
disabled={localOnly}
|
|
106
|
+
onChange={(e) => setSaveRelations(e.target.checked)}
|
|
107
|
+
/>{" "}
|
|
108
|
+
Save members & opening hours
|
|
109
|
+
</span>
|
|
110
|
+
</label>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div className="card">
|
|
114
|
+
<h2>Payload</h2>
|
|
115
|
+
<form className="form" onSubmit={handleSubmit}>
|
|
116
|
+
<label className="form__label">
|
|
117
|
+
<span>JSON</span>
|
|
118
|
+
<textarea
|
|
119
|
+
className="form__textarea"
|
|
120
|
+
value={jsonText}
|
|
121
|
+
onChange={(e) => setJsonText(e.target.value)}
|
|
122
|
+
spellCheck={false}
|
|
123
|
+
rows={16}
|
|
124
|
+
/>
|
|
125
|
+
</label>
|
|
126
|
+
<button className="btn btn--primary" disabled={busy}>
|
|
127
|
+
{busy ? "Working…" : "Create"}
|
|
128
|
+
</button>
|
|
129
|
+
</form>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{error ? (
|
|
133
|
+
<div className="card card--error" role="alert">
|
|
134
|
+
<h2>Error</h2>
|
|
135
|
+
<pre className="pre-block">{error}</pre>
|
|
136
|
+
</div>
|
|
137
|
+
) : null}
|
|
138
|
+
|
|
139
|
+
{output ? (
|
|
140
|
+
<div className="card card--success">
|
|
141
|
+
<h2>Result</h2>
|
|
142
|
+
<pre className="pre-block">{output}</pre>
|
|
143
|
+
</div>
|
|
144
|
+
) : null}
|
|
145
|
+
</>
|
|
146
|
+
);
|
|
147
|
+
}
|