@joai/react 1.0.0
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/README.md +9 -0
- package/dist/index.d.mts +430 -0
- package/dist/index.mjs +1788 -0
- package/package.json +41 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1788 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { containedImageDimensions, containedImageDimensions as containedImageDimensions$1, cropOutputDimensions, cropOutputDimensions as cropOutputDimensions$1, getLoginUrl, imageCropRectangle, imageCropRectangle as imageCropRectangle$1, validateImageDimensions } from "@joai/core";
|
|
5
|
+
import { createClient, getAuthFragmentToken } from "@joai/sdk";
|
|
6
|
+
//#region src/guest.ts
|
|
7
|
+
function safeGet(key) {
|
|
8
|
+
try {
|
|
9
|
+
return localStorage.getItem(key);
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function safeSet(key, value) {
|
|
15
|
+
try {
|
|
16
|
+
localStorage.setItem(key, value);
|
|
17
|
+
} catch {}
|
|
18
|
+
}
|
|
19
|
+
function safeRemove(key) {
|
|
20
|
+
try {
|
|
21
|
+
localStorage.removeItem(key);
|
|
22
|
+
} catch {}
|
|
23
|
+
}
|
|
24
|
+
function getOrCreateGuestId(storageKey = "joai_embed_guest") {
|
|
25
|
+
if (typeof window === "undefined") return "";
|
|
26
|
+
const existing = safeGet(storageKey);
|
|
27
|
+
if (existing) return existing;
|
|
28
|
+
const id = crypto.randomUUID();
|
|
29
|
+
safeSet(storageKey, id);
|
|
30
|
+
return id;
|
|
31
|
+
}
|
|
32
|
+
function loadRoom(storageKey) {
|
|
33
|
+
return safeGet(storageKey);
|
|
34
|
+
}
|
|
35
|
+
function saveRoom(storageKey, roomId) {
|
|
36
|
+
safeSet(storageKey, roomId);
|
|
37
|
+
}
|
|
38
|
+
function clearRoom(storageKey) {
|
|
39
|
+
safeRemove(storageKey);
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/stream-connection.ts
|
|
43
|
+
function openEventSourceStream(url, onData, options) {
|
|
44
|
+
const es = new EventSource(url, { withCredentials: options?.withCredentials ?? false });
|
|
45
|
+
es.onmessage = (event) => {
|
|
46
|
+
try {
|
|
47
|
+
onData(JSON.parse(event.data));
|
|
48
|
+
} catch (e) {
|
|
49
|
+
options?.onError?.(e);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
es.onerror = (event) => {
|
|
53
|
+
options?.onConnectionError?.(event);
|
|
54
|
+
};
|
|
55
|
+
return () => es.close();
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/components.tsx
|
|
59
|
+
function EmbedChatList({ items, agentName, awaitingResponse, classNames = {}, renderEmptyState, renderAgentContent }) {
|
|
60
|
+
const containerRef = useRef(null);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const el = containerRef.current;
|
|
63
|
+
if (!el) return;
|
|
64
|
+
if (el.scrollHeight - el.scrollTop - el.clientHeight < 120) el.scrollTop = el.scrollHeight;
|
|
65
|
+
}, [items, awaitingResponse]);
|
|
66
|
+
if (items.length === 0 && !awaitingResponse) return renderEmptyState ? renderEmptyState() : /* @__PURE__ */ jsx("div", {
|
|
67
|
+
className: classNames.container,
|
|
68
|
+
children: /* @__PURE__ */ jsxs("p", { children: [
|
|
69
|
+
"Send ",
|
|
70
|
+
agentName,
|
|
71
|
+
" a message to start chatting"
|
|
72
|
+
] })
|
|
73
|
+
});
|
|
74
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
75
|
+
ref: containerRef,
|
|
76
|
+
className: classNames.container,
|
|
77
|
+
role: "log",
|
|
78
|
+
"aria-live": "polite",
|
|
79
|
+
children: [items.map((item, i) => /* @__PURE__ */ jsx(EmbedChatBubble, {
|
|
80
|
+
item,
|
|
81
|
+
classNames,
|
|
82
|
+
renderAgentContent
|
|
83
|
+
}, i)), awaitingResponse && /* @__PURE__ */ jsx(EmbedThinkingIndicator, {
|
|
84
|
+
wrapperClassName: classNames.thinkingWrapper,
|
|
85
|
+
dotClassName: classNames.dot
|
|
86
|
+
})]
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function EmbedChatBubble({ item, classNames, renderAgentContent }) {
|
|
90
|
+
if (item.byUser) return /* @__PURE__ */ jsx("div", {
|
|
91
|
+
className: classNames.userBubble,
|
|
92
|
+
children: item.content
|
|
93
|
+
});
|
|
94
|
+
return /* @__PURE__ */ jsx("div", {
|
|
95
|
+
className: item.error ? classNames.errorBubble : classNames.agentBubble,
|
|
96
|
+
children: renderAgentContent ? renderAgentContent(item) : /* @__PURE__ */ jsxs(Fragment, { children: [item.content, item.streaming && /* @__PURE__ */ jsx("span", { className: "inline-block w-1 h-3 ml-1 bg-current animate-pulse rounded-full" })] })
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function EmbedThinkingIndicator({ wrapperClassName, dotClassName }) {
|
|
100
|
+
return /* @__PURE__ */ jsx("div", {
|
|
101
|
+
className: wrapperClassName,
|
|
102
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
103
|
+
className: "inline-flex gap-1",
|
|
104
|
+
children: [
|
|
105
|
+
/* @__PURE__ */ jsx("span", {
|
|
106
|
+
className: dotClassName,
|
|
107
|
+
style: { animationDelay: "0ms" }
|
|
108
|
+
}),
|
|
109
|
+
/* @__PURE__ */ jsx("span", {
|
|
110
|
+
className: dotClassName,
|
|
111
|
+
style: { animationDelay: "150ms" }
|
|
112
|
+
}),
|
|
113
|
+
/* @__PURE__ */ jsx("span", {
|
|
114
|
+
className: dotClassName,
|
|
115
|
+
style: { animationDelay: "300ms" }
|
|
116
|
+
})
|
|
117
|
+
]
|
|
118
|
+
})
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function EmbedChatInput({ onSend, disabled, placeholder = "Type a message...", maxLength = 4e3, classNames = {}, renderSendButton }) {
|
|
122
|
+
const [text, setText] = useState("");
|
|
123
|
+
const textareaRef = useRef(null);
|
|
124
|
+
function handleSend() {
|
|
125
|
+
const trimmed = text.trim();
|
|
126
|
+
if (!trimmed || disabled) return;
|
|
127
|
+
onSend(trimmed);
|
|
128
|
+
setText("");
|
|
129
|
+
if (textareaRef.current) textareaRef.current.style.height = "auto";
|
|
130
|
+
}
|
|
131
|
+
function handleKeyDown(e) {
|
|
132
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
handleSend();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function handleInput() {
|
|
138
|
+
if (textareaRef.current) {
|
|
139
|
+
textareaRef.current.style.height = "auto";
|
|
140
|
+
textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 120) + "px";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
144
|
+
className: classNames.container,
|
|
145
|
+
children: [/* @__PURE__ */ jsx("textarea", {
|
|
146
|
+
ref: textareaRef,
|
|
147
|
+
value: text,
|
|
148
|
+
onChange: (e) => setText(e.target.value),
|
|
149
|
+
onKeyDown: handleKeyDown,
|
|
150
|
+
onInput: handleInput,
|
|
151
|
+
placeholder,
|
|
152
|
+
"aria-label": "Message input",
|
|
153
|
+
maxLength,
|
|
154
|
+
rows: 1,
|
|
155
|
+
disabled,
|
|
156
|
+
className: classNames.textarea
|
|
157
|
+
}), renderSendButton({
|
|
158
|
+
onClick: handleSend,
|
|
159
|
+
disabled: disabled || !text.trim()
|
|
160
|
+
})]
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/use-embed-chat.ts
|
|
165
|
+
const RESPONSE_TIMEOUT_MS = 3e4;
|
|
166
|
+
function useEmbedChat({ agentUuid, http, cortexBaseUrl, guestId, roomStorageKey, enabled = true, extraPayload }) {
|
|
167
|
+
const [items, setItems] = useState([]);
|
|
168
|
+
const [sending, setSending] = useState(false);
|
|
169
|
+
const [awaitingResponse, setAwaitingResponse] = useState(false);
|
|
170
|
+
const [roomId, setRoomId] = useState(null);
|
|
171
|
+
const [connectionError, setConnectionError] = useState(false);
|
|
172
|
+
const roomIdRef = useRef(null);
|
|
173
|
+
roomIdRef.current = roomId;
|
|
174
|
+
const extraPayloadRef = useRef(extraPayload);
|
|
175
|
+
extraPayloadRef.current = extraPayload;
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (!roomId) return;
|
|
178
|
+
setConnectionError(false);
|
|
179
|
+
return openEventSourceStream(`${cortexBaseUrl}/agents/stream/${roomId}`, (raw) => {
|
|
180
|
+
setConnectionError(false);
|
|
181
|
+
const data = raw;
|
|
182
|
+
if (data.type === "response:chunk") {
|
|
183
|
+
setAwaitingResponse(false);
|
|
184
|
+
setItems((prev) => {
|
|
185
|
+
const last = prev[prev.length - 1];
|
|
186
|
+
if (last && last.streaming && !last.byUser) {
|
|
187
|
+
const updated = [...prev];
|
|
188
|
+
updated[updated.length - 1] = {
|
|
189
|
+
...last,
|
|
190
|
+
content: last.content + (data.content ?? "")
|
|
191
|
+
};
|
|
192
|
+
return updated;
|
|
193
|
+
}
|
|
194
|
+
return [...prev, {
|
|
195
|
+
content: data.content ?? "",
|
|
196
|
+
byUser: false,
|
|
197
|
+
streaming: true
|
|
198
|
+
}];
|
|
199
|
+
});
|
|
200
|
+
} else if (data.type === "response") {
|
|
201
|
+
setAwaitingResponse(false);
|
|
202
|
+
setItems((prev) => {
|
|
203
|
+
const updated = [...prev];
|
|
204
|
+
const lastIdx = updated.length - 1;
|
|
205
|
+
if (lastIdx >= 0 && updated[lastIdx].streaming) updated[lastIdx] = {
|
|
206
|
+
...updated[lastIdx],
|
|
207
|
+
content: data.content ?? updated[lastIdx].content,
|
|
208
|
+
streaming: false
|
|
209
|
+
};
|
|
210
|
+
else updated.push({
|
|
211
|
+
content: data.content ?? "",
|
|
212
|
+
byUser: false
|
|
213
|
+
});
|
|
214
|
+
return updated;
|
|
215
|
+
});
|
|
216
|
+
} else if (data.type === "complete") {
|
|
217
|
+
setAwaitingResponse(false);
|
|
218
|
+
setItems((prev) => prev.map((item) => item.streaming ? {
|
|
219
|
+
...item,
|
|
220
|
+
streaming: false
|
|
221
|
+
} : item));
|
|
222
|
+
}
|
|
223
|
+
}, { onConnectionError: () => setConnectionError(true) });
|
|
224
|
+
}, [roomId, cortexBaseUrl]);
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
if (!awaitingResponse) return;
|
|
227
|
+
const timer = setTimeout(() => {
|
|
228
|
+
setAwaitingResponse(false);
|
|
229
|
+
setItems((prev) => [...prev, {
|
|
230
|
+
content: "Response timed out. Please try again.",
|
|
231
|
+
byUser: false,
|
|
232
|
+
error: true
|
|
233
|
+
}]);
|
|
234
|
+
}, RESPONSE_TIMEOUT_MS);
|
|
235
|
+
return () => clearTimeout(timer);
|
|
236
|
+
}, [awaitingResponse]);
|
|
237
|
+
return {
|
|
238
|
+
items,
|
|
239
|
+
setItems,
|
|
240
|
+
sending,
|
|
241
|
+
awaitingResponse,
|
|
242
|
+
roomId,
|
|
243
|
+
setRoomId,
|
|
244
|
+
send: useCallback(async (message) => {
|
|
245
|
+
if (!enabled || sending || !agentUuid) return;
|
|
246
|
+
setItems((prev) => [...prev, {
|
|
247
|
+
content: message,
|
|
248
|
+
byUser: true
|
|
249
|
+
}]);
|
|
250
|
+
setSending(true);
|
|
251
|
+
setConnectionError(false);
|
|
252
|
+
try {
|
|
253
|
+
const res = await http.post(`integrations/embed/${agentUuid}/execute`, {
|
|
254
|
+
message,
|
|
255
|
+
guestId,
|
|
256
|
+
...extraPayloadRef.current
|
|
257
|
+
});
|
|
258
|
+
if (!res.ok) {
|
|
259
|
+
const msg = res.data && typeof res.data === "object" && "message" in res.data ? res.data.message : "Something went wrong. Please try again.";
|
|
260
|
+
setItems((prev) => [...prev, {
|
|
261
|
+
content: msg,
|
|
262
|
+
byUser: false,
|
|
263
|
+
error: true
|
|
264
|
+
}]);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
setAwaitingResponse(true);
|
|
268
|
+
const newRoomId = res.data?.roomId;
|
|
269
|
+
if (newRoomId && !roomIdRef.current) {
|
|
270
|
+
setRoomId(newRoomId);
|
|
271
|
+
saveRoom(roomStorageKey, newRoomId);
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
setItems((prev) => [...prev, {
|
|
275
|
+
content: "Something went wrong. Please try again.",
|
|
276
|
+
byUser: false,
|
|
277
|
+
error: true
|
|
278
|
+
}]);
|
|
279
|
+
} finally {
|
|
280
|
+
setSending(false);
|
|
281
|
+
}
|
|
282
|
+
}, [
|
|
283
|
+
enabled,
|
|
284
|
+
sending,
|
|
285
|
+
agentUuid,
|
|
286
|
+
http,
|
|
287
|
+
guestId,
|
|
288
|
+
roomStorageKey
|
|
289
|
+
]),
|
|
290
|
+
connectionError
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region src/content/context.ts
|
|
295
|
+
const ContentContext = createContext(null);
|
|
296
|
+
function useContent() {
|
|
297
|
+
const context = useContext(ContentContext);
|
|
298
|
+
if (!context) throw new Error("useContent must be used within a ContentProvider.");
|
|
299
|
+
return context;
|
|
300
|
+
}
|
|
301
|
+
//#endregion
|
|
302
|
+
//#region src/content/content-cache.ts
|
|
303
|
+
const contentCache = /* @__PURE__ */ new Map();
|
|
304
|
+
const pendingLoads = /* @__PURE__ */ new Map();
|
|
305
|
+
function cacheContent(content) {
|
|
306
|
+
contentCache.set(`id:${content.id}`, content);
|
|
307
|
+
contentCache.set(`slug:${content.slug}`, content);
|
|
308
|
+
}
|
|
309
|
+
async function loadContent(client, contentId, publicSlug) {
|
|
310
|
+
const target = contentId ?? publicSlug;
|
|
311
|
+
if (!target) return null;
|
|
312
|
+
const key = contentId ? `id:${contentId}` : `slug:${publicSlug}`;
|
|
313
|
+
const cached = contentCache.get(key);
|
|
314
|
+
if (cached) return cached;
|
|
315
|
+
const pending = pendingLoads.get(key);
|
|
316
|
+
if (pending) return pending;
|
|
317
|
+
const request = (contentId ? client.content.get(target) : client.content.getPublic(target)).then((result) => result.ok && result.data ? result.data : null).then((content) => {
|
|
318
|
+
if (content) cacheContent(content);
|
|
319
|
+
return content;
|
|
320
|
+
}).finally(() => pendingLoads.delete(key));
|
|
321
|
+
pendingLoads.set(key, request);
|
|
322
|
+
return request;
|
|
323
|
+
}
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/content/state.ts
|
|
326
|
+
function contentValues(content) {
|
|
327
|
+
return Object.fromEntries(content?.version?.entries.map((entry) => [entry.key, entry.value]) ?? []);
|
|
328
|
+
}
|
|
329
|
+
function contentPatches(content, values) {
|
|
330
|
+
const original = contentValues(content);
|
|
331
|
+
return Object.entries(values).filter(([key, value]) => JSON.stringify(value) !== JSON.stringify(original[key])).map(([key, value]) => ({
|
|
332
|
+
key,
|
|
333
|
+
value
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region src/content/use-unsaved-navigation.ts
|
|
338
|
+
const navigationApi = () => {
|
|
339
|
+
if (typeof window === "undefined") return null;
|
|
340
|
+
return window.navigation ?? null;
|
|
341
|
+
};
|
|
342
|
+
const useUnsavedNavigation = (shouldBlock, confirmLeave) => {
|
|
343
|
+
const allowed = useRef(false);
|
|
344
|
+
useEffect(() => {
|
|
345
|
+
if (!shouldBlock || typeof window === "undefined" || typeof document === "undefined") return;
|
|
346
|
+
allowed.current = false;
|
|
347
|
+
const beforeUnload = (event) => {
|
|
348
|
+
if (allowed.current) return;
|
|
349
|
+
event.preventDefault();
|
|
350
|
+
event.returnValue = "";
|
|
351
|
+
};
|
|
352
|
+
const confirmNavigation = () => {
|
|
353
|
+
if (!confirmLeave()) return false;
|
|
354
|
+
allowed.current = true;
|
|
355
|
+
return true;
|
|
356
|
+
};
|
|
357
|
+
const navigation = navigationApi();
|
|
358
|
+
const navigate = ((event) => {
|
|
359
|
+
if (allowed.current || event.hashChange || !event.cancelable) return;
|
|
360
|
+
const destination = event.destination?.url;
|
|
361
|
+
if (destination) {
|
|
362
|
+
const current = new URL(window.location.href);
|
|
363
|
+
const next = new URL(destination, current);
|
|
364
|
+
if (current.origin === next.origin && current.pathname === next.pathname && current.search === next.search) return;
|
|
365
|
+
}
|
|
366
|
+
if (!confirmNavigation()) event.preventDefault();
|
|
367
|
+
});
|
|
368
|
+
const click = (event) => {
|
|
369
|
+
if (allowed.current || event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
370
|
+
const target = event.target instanceof Element ? event.target.closest("a[href]") : null;
|
|
371
|
+
if (!target || target.target === "_blank" || target.hasAttribute("download")) return;
|
|
372
|
+
if (target.hasAttribute("data-content-navigation-allowed")) {
|
|
373
|
+
allowed.current = true;
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const current = new URL(window.location.href);
|
|
377
|
+
const next = new URL(target.href, current);
|
|
378
|
+
if (current.origin === next.origin && current.pathname === next.pathname && current.search === next.search) return;
|
|
379
|
+
if (!confirmNavigation()) {
|
|
380
|
+
event.preventDefault();
|
|
381
|
+
event.stopImmediatePropagation();
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
window.addEventListener("beforeunload", beforeUnload);
|
|
385
|
+
if (navigation) navigation.addEventListener("navigate", navigate);
|
|
386
|
+
else document.addEventListener("click", click, true);
|
|
387
|
+
return () => {
|
|
388
|
+
window.removeEventListener("beforeunload", beforeUnload);
|
|
389
|
+
if (navigation) navigation.removeEventListener("navigate", navigate);
|
|
390
|
+
else document.removeEventListener("click", click, true);
|
|
391
|
+
};
|
|
392
|
+
}, [shouldBlock]);
|
|
393
|
+
};
|
|
394
|
+
//#endregion
|
|
395
|
+
//#region src/content/provider.tsx
|
|
396
|
+
const DEFAULT_MESSAGES = {
|
|
397
|
+
conflict: "This content changed elsewhere. Your edits are preserved on the latest version. Review them and try again.",
|
|
398
|
+
loadError: "Content could not be loaded.",
|
|
399
|
+
publishError: "Changes could not be published. Try again.",
|
|
400
|
+
published: "Changes published.",
|
|
401
|
+
saveError: "Changes could not be saved. Try again.",
|
|
402
|
+
saved: "Draft saved.",
|
|
403
|
+
sessionExpired: "Your session expired. Sign in again to continue.",
|
|
404
|
+
unsavedNavigation: "You have unsaved changes. Do you really want to leave this page?"
|
|
405
|
+
};
|
|
406
|
+
const DEFAULT_IMAGE_PICKER_LABELS = {
|
|
407
|
+
applyCrop: "Apply crop",
|
|
408
|
+
back: "Back",
|
|
409
|
+
choose: "Choose image",
|
|
410
|
+
close: "Close",
|
|
411
|
+
cropDescription: "Adjust the position and zoom before uploading.",
|
|
412
|
+
cropTitle: "Crop image",
|
|
413
|
+
dimensionsTooLarge: "The image dimensions are too large.",
|
|
414
|
+
dimensionsTooSmall: "The image dimensions are too small.",
|
|
415
|
+
drop: "Drop an image here",
|
|
416
|
+
empty: "No images are available yet.",
|
|
417
|
+
field: "Editable image",
|
|
418
|
+
invalidType: "Choose an image file.",
|
|
419
|
+
libraryDescription: "Choose an existing image or upload a new one.",
|
|
420
|
+
libraryTitle: "Image library",
|
|
421
|
+
loadError: "Images could not be loaded.",
|
|
422
|
+
loading: "Loading images…",
|
|
423
|
+
positionHorizontal: "Horizontal position",
|
|
424
|
+
positionVertical: "Vertical position",
|
|
425
|
+
processing: "Preparing image…",
|
|
426
|
+
retry: "Try again",
|
|
427
|
+
tooLarge: "The image is too large.",
|
|
428
|
+
upload: "Upload image",
|
|
429
|
+
uploadError: "The image could not be uploaded.",
|
|
430
|
+
uploading: "Uploading…",
|
|
431
|
+
zoom: "Zoom"
|
|
432
|
+
};
|
|
433
|
+
const isContentConflict = (result) => {
|
|
434
|
+
return result.original?.status === 409 || Boolean(result.errors && "expectedVersion" in result.errors);
|
|
435
|
+
};
|
|
436
|
+
const isUnauthorized = (result) => result.original?.status === 401 || result.original?.status === 403;
|
|
437
|
+
const valuesWithDraft = (source, canEdit, contentId, draftStore) => {
|
|
438
|
+
const values = contentValues(source);
|
|
439
|
+
if (!canEdit || !draftStore) return values;
|
|
440
|
+
try {
|
|
441
|
+
const draft = draftStore.get();
|
|
442
|
+
if (!draft || draft.contentId && contentId && draft.contentId !== contentId) return values;
|
|
443
|
+
return {
|
|
444
|
+
...values,
|
|
445
|
+
...draft.values
|
|
446
|
+
};
|
|
447
|
+
} catch {
|
|
448
|
+
return values;
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
const clearDraft = (draftStore) => {
|
|
452
|
+
try {
|
|
453
|
+
draftStore?.clear();
|
|
454
|
+
} catch {}
|
|
455
|
+
};
|
|
456
|
+
const persistDraft = (draftStore, contentId, values) => {
|
|
457
|
+
try {
|
|
458
|
+
draftStore?.set({
|
|
459
|
+
contentId,
|
|
460
|
+
values
|
|
461
|
+
});
|
|
462
|
+
} catch {}
|
|
463
|
+
};
|
|
464
|
+
function ContentProvider({ children, client, contentId, publicSlug, initialContent, canEdit = false, canPublish = false, teamSlug, confirmLeave, draftStore, imagePickerLabels: imagePickerLabelOverrides, messages: messageOverrides, onUnauthorized }) {
|
|
465
|
+
const messages = {
|
|
466
|
+
...DEFAULT_MESSAGES,
|
|
467
|
+
...messageOverrides
|
|
468
|
+
};
|
|
469
|
+
const imagePickerLabels = {
|
|
470
|
+
...DEFAULT_IMAGE_PICKER_LABELS,
|
|
471
|
+
...imagePickerLabelOverrides
|
|
472
|
+
};
|
|
473
|
+
const [content, setContent] = useState(initialContent ?? null);
|
|
474
|
+
const [values, setValues] = useState(() => valuesWithDraft(initialContent ?? null, canEdit, contentId, draftStore));
|
|
475
|
+
const [isLoading, setIsLoading] = useState(!initialContent && Boolean(contentId || publicSlug));
|
|
476
|
+
const [operation, setOperation] = useState(null);
|
|
477
|
+
const [isEditing, setEditing] = useState(false);
|
|
478
|
+
const [error, setError] = useState(null);
|
|
479
|
+
const [message, setMessage] = useState(null);
|
|
480
|
+
useEffect(() => {
|
|
481
|
+
if (initialContent) {
|
|
482
|
+
cacheContent(initialContent);
|
|
483
|
+
setContent(initialContent);
|
|
484
|
+
setValues(valuesWithDraft(initialContent, canEdit, contentId, draftStore));
|
|
485
|
+
setIsLoading(false);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const load = async () => {
|
|
489
|
+
if (!contentId && !publicSlug) return;
|
|
490
|
+
setIsLoading(true);
|
|
491
|
+
setError(null);
|
|
492
|
+
setMessage(null);
|
|
493
|
+
try {
|
|
494
|
+
const nextContent = await loadContent(client, contentId, publicSlug);
|
|
495
|
+
if (!nextContent) setError(messages.loadError);
|
|
496
|
+
else {
|
|
497
|
+
setContent(nextContent);
|
|
498
|
+
setValues(valuesWithDraft(nextContent, canEdit, contentId, draftStore));
|
|
499
|
+
}
|
|
500
|
+
} catch {
|
|
501
|
+
setError(messages.loadError);
|
|
502
|
+
} finally {
|
|
503
|
+
setIsLoading(false);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
load();
|
|
507
|
+
}, [
|
|
508
|
+
contentId,
|
|
509
|
+
initialContent?.id,
|
|
510
|
+
initialContent?.version?.number,
|
|
511
|
+
publicSlug
|
|
512
|
+
]);
|
|
513
|
+
const setValue = useCallback((key, value) => {
|
|
514
|
+
setError(null);
|
|
515
|
+
setMessage(null);
|
|
516
|
+
setValues((current) => ({
|
|
517
|
+
...current,
|
|
518
|
+
[key]: value
|
|
519
|
+
}));
|
|
520
|
+
}, []);
|
|
521
|
+
const updateEditing = useCallback((editing) => {
|
|
522
|
+
if (editing) {
|
|
523
|
+
setError(null);
|
|
524
|
+
setMessage(null);
|
|
525
|
+
}
|
|
526
|
+
setEditing(editing);
|
|
527
|
+
}, []);
|
|
528
|
+
const isDirty = contentPatches(content, values).length > 0;
|
|
529
|
+
const isSaving = operation !== null;
|
|
530
|
+
const versionNumber = content?.version?.number;
|
|
531
|
+
const valuesSignature = JSON.stringify(values);
|
|
532
|
+
const requestLeave = useCallback(() => confirmLeave ? confirmLeave() : window.confirm(messages.unsavedNavigation), [messages.unsavedNavigation]);
|
|
533
|
+
useUnsavedNavigation(canEdit && isDirty, requestLeave);
|
|
534
|
+
useEffect(() => {
|
|
535
|
+
if (!canEdit || !draftStore || !versionNumber) return;
|
|
536
|
+
if (isDirty) persistDraft(draftStore, contentId, JSON.parse(valuesSignature));
|
|
537
|
+
else clearDraft(draftStore);
|
|
538
|
+
}, [
|
|
539
|
+
canEdit,
|
|
540
|
+
contentId,
|
|
541
|
+
isDirty,
|
|
542
|
+
valuesSignature,
|
|
543
|
+
versionNumber
|
|
544
|
+
]);
|
|
545
|
+
const recoverConflict = useCallback(async () => {
|
|
546
|
+
if (!contentId) return false;
|
|
547
|
+
try {
|
|
548
|
+
const latest = await client.content.get(contentId);
|
|
549
|
+
if (!latest.ok || !latest.data) return false;
|
|
550
|
+
cacheContent(latest.data);
|
|
551
|
+
setContent(latest.data);
|
|
552
|
+
setError(messages.conflict);
|
|
553
|
+
return true;
|
|
554
|
+
} catch {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
}, [contentId, messages.conflict]);
|
|
558
|
+
const cancel = useCallback(() => {
|
|
559
|
+
setValues(contentValues(content));
|
|
560
|
+
clearDraft(draftStore);
|
|
561
|
+
setError(null);
|
|
562
|
+
setMessage(null);
|
|
563
|
+
setEditing(false);
|
|
564
|
+
}, [content]);
|
|
565
|
+
const save = useCallback(async () => {
|
|
566
|
+
if (!content?.version || !contentId || !canEdit) return false;
|
|
567
|
+
const patches = contentPatches(content, values);
|
|
568
|
+
if (patches.length === 0) return true;
|
|
569
|
+
setOperation("save");
|
|
570
|
+
setError(null);
|
|
571
|
+
setMessage(null);
|
|
572
|
+
try {
|
|
573
|
+
const result = await client.content.patch(contentId, {
|
|
574
|
+
expectedVersion: content.version.number,
|
|
575
|
+
patches,
|
|
576
|
+
origin: "inlineEditor"
|
|
577
|
+
});
|
|
578
|
+
if (!result.ok || !result.data) {
|
|
579
|
+
if (isUnauthorized(result)) {
|
|
580
|
+
setError(messages.sessionExpired);
|
|
581
|
+
onUnauthorized?.();
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
if (isContentConflict(result) && await recoverConflict()) return false;
|
|
585
|
+
setError(messages.saveError);
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
const nextContent = {
|
|
589
|
+
...content,
|
|
590
|
+
version: result.data,
|
|
591
|
+
updatedAt: result.data.updatedAt
|
|
592
|
+
};
|
|
593
|
+
cacheContent(nextContent);
|
|
594
|
+
setContent(nextContent);
|
|
595
|
+
setValues(contentValues(nextContent));
|
|
596
|
+
clearDraft(draftStore);
|
|
597
|
+
setMessage(messages.saved);
|
|
598
|
+
return true;
|
|
599
|
+
} catch {
|
|
600
|
+
setError(messages.saveError);
|
|
601
|
+
return false;
|
|
602
|
+
} finally {
|
|
603
|
+
setOperation(null);
|
|
604
|
+
}
|
|
605
|
+
}, [
|
|
606
|
+
canEdit,
|
|
607
|
+
content,
|
|
608
|
+
contentId,
|
|
609
|
+
messages.saveError,
|
|
610
|
+
messages.saved,
|
|
611
|
+
messages.sessionExpired,
|
|
612
|
+
recoverConflict,
|
|
613
|
+
values
|
|
614
|
+
]);
|
|
615
|
+
const publish = useCallback(async () => {
|
|
616
|
+
if (!content?.version || !contentId || !canPublish) return false;
|
|
617
|
+
const patches = contentPatches(content, values);
|
|
618
|
+
setOperation("publish");
|
|
619
|
+
setError(null);
|
|
620
|
+
setMessage(null);
|
|
621
|
+
try {
|
|
622
|
+
const result = patches.length > 0 ? await client.content.patch(contentId, {
|
|
623
|
+
expectedVersion: content.version.number,
|
|
624
|
+
patches,
|
|
625
|
+
origin: "inlineEditor",
|
|
626
|
+
publish: true
|
|
627
|
+
}) : await client.content.publish(contentId, content.version.number);
|
|
628
|
+
if (!result.ok || !result.data) {
|
|
629
|
+
if (isUnauthorized(result)) {
|
|
630
|
+
setError(messages.sessionExpired);
|
|
631
|
+
onUnauthorized?.();
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
if (isContentConflict(result) && await recoverConflict()) return false;
|
|
635
|
+
setError(messages.publishError);
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
const nextContent = {
|
|
639
|
+
...content,
|
|
640
|
+
version: result.data,
|
|
641
|
+
updatedAt: result.data.updatedAt
|
|
642
|
+
};
|
|
643
|
+
cacheContent(nextContent);
|
|
644
|
+
setContent(nextContent);
|
|
645
|
+
setValues(contentValues(nextContent));
|
|
646
|
+
clearDraft(draftStore);
|
|
647
|
+
setEditing(false);
|
|
648
|
+
setMessage(messages.published);
|
|
649
|
+
return true;
|
|
650
|
+
} catch {
|
|
651
|
+
setError(messages.publishError);
|
|
652
|
+
return false;
|
|
653
|
+
} finally {
|
|
654
|
+
setOperation(null);
|
|
655
|
+
}
|
|
656
|
+
}, [
|
|
657
|
+
canPublish,
|
|
658
|
+
content,
|
|
659
|
+
contentId,
|
|
660
|
+
messages.publishError,
|
|
661
|
+
messages.published,
|
|
662
|
+
messages.sessionExpired,
|
|
663
|
+
recoverConflict,
|
|
664
|
+
values
|
|
665
|
+
]);
|
|
666
|
+
const context = useMemo(() => ({
|
|
667
|
+
content,
|
|
668
|
+
values,
|
|
669
|
+
isLoading,
|
|
670
|
+
isSaving,
|
|
671
|
+
operation,
|
|
672
|
+
isEditing,
|
|
673
|
+
isDirty,
|
|
674
|
+
canEdit,
|
|
675
|
+
canPublish,
|
|
676
|
+
teamSlug,
|
|
677
|
+
media: client.media,
|
|
678
|
+
error,
|
|
679
|
+
message,
|
|
680
|
+
imagePickerLabels,
|
|
681
|
+
cancel,
|
|
682
|
+
setEditing: updateEditing,
|
|
683
|
+
setValue,
|
|
684
|
+
save,
|
|
685
|
+
publish
|
|
686
|
+
}), [
|
|
687
|
+
cancel,
|
|
688
|
+
canEdit,
|
|
689
|
+
canPublish,
|
|
690
|
+
content,
|
|
691
|
+
error,
|
|
692
|
+
imagePickerLabels.applyCrop,
|
|
693
|
+
imagePickerLabels.back,
|
|
694
|
+
imagePickerLabels.choose,
|
|
695
|
+
imagePickerLabels.close,
|
|
696
|
+
imagePickerLabels.cropDescription,
|
|
697
|
+
imagePickerLabels.cropTitle,
|
|
698
|
+
imagePickerLabels.dimensionsTooLarge,
|
|
699
|
+
imagePickerLabels.dimensionsTooSmall,
|
|
700
|
+
imagePickerLabels.drop,
|
|
701
|
+
imagePickerLabels.empty,
|
|
702
|
+
imagePickerLabels.field,
|
|
703
|
+
imagePickerLabels.invalidType,
|
|
704
|
+
imagePickerLabels.libraryDescription,
|
|
705
|
+
imagePickerLabels.libraryTitle,
|
|
706
|
+
imagePickerLabels.loadError,
|
|
707
|
+
imagePickerLabels.loading,
|
|
708
|
+
imagePickerLabels.positionHorizontal,
|
|
709
|
+
imagePickerLabels.positionVertical,
|
|
710
|
+
imagePickerLabels.processing,
|
|
711
|
+
imagePickerLabels.retry,
|
|
712
|
+
imagePickerLabels.tooLarge,
|
|
713
|
+
imagePickerLabels.upload,
|
|
714
|
+
imagePickerLabels.uploadError,
|
|
715
|
+
imagePickerLabels.uploading,
|
|
716
|
+
imagePickerLabels.zoom,
|
|
717
|
+
isDirty,
|
|
718
|
+
isEditing,
|
|
719
|
+
isLoading,
|
|
720
|
+
isSaving,
|
|
721
|
+
message,
|
|
722
|
+
operation,
|
|
723
|
+
publish,
|
|
724
|
+
save,
|
|
725
|
+
setValue,
|
|
726
|
+
teamSlug,
|
|
727
|
+
updateEditing,
|
|
728
|
+
values
|
|
729
|
+
]);
|
|
730
|
+
return /* @__PURE__ */ jsx(ContentContext.Provider, {
|
|
731
|
+
value: context,
|
|
732
|
+
children
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
//#endregion
|
|
736
|
+
//#region src/content/editable-content.tsx
|
|
737
|
+
function EditableContent({ field, as: Tag = "span", className, placeholder = "Add text", multiline = false, editorClassName, label }) {
|
|
738
|
+
const { values, isEditing, canEdit, setValue } = useContent();
|
|
739
|
+
const value = values[field];
|
|
740
|
+
const text = typeof value === "string" || typeof value === "number" ? String(value) : "";
|
|
741
|
+
const fieldLabel = label ?? field;
|
|
742
|
+
if (!canEdit || !isEditing) return /* @__PURE__ */ jsx(Tag, {
|
|
743
|
+
className,
|
|
744
|
+
children: text
|
|
745
|
+
});
|
|
746
|
+
const editorClasses = `${className ?? ""} ${editorClassName ?? ""} block min-h-[2.5rem] w-full rounded-md border border-dashed border-blue-400/80 bg-white/95 px-3 py-2 [font:inherit] text-inherit shadow-sm outline-none transition hover:border-blue-500 hover:bg-blue-50/60 focus:border-blue-600 focus:bg-white focus:ring-2 focus:ring-blue-500/30`.trim();
|
|
747
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
748
|
+
className: "group/content-field relative my-1 block",
|
|
749
|
+
"data-content-editable": field,
|
|
750
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
751
|
+
className: "pointer-events-none absolute -top-2 left-2 z-10 rounded bg-blue-600 px-1.5 py-0.5 text-[10px] font-semibold uppercase leading-none tracking-wide text-white opacity-90 shadow-sm transition group-hover/content-field:opacity-100",
|
|
752
|
+
children: fieldLabel
|
|
753
|
+
}), multiline ? /* @__PURE__ */ jsx("textarea", {
|
|
754
|
+
"aria-label": fieldLabel,
|
|
755
|
+
className: `${editorClasses} min-h-[7rem] resize-y`,
|
|
756
|
+
placeholder,
|
|
757
|
+
value: text,
|
|
758
|
+
onChange: (event) => setValue(field, event.target.value)
|
|
759
|
+
}) : /* @__PURE__ */ jsx("input", {
|
|
760
|
+
"aria-label": fieldLabel,
|
|
761
|
+
className: editorClasses,
|
|
762
|
+
placeholder,
|
|
763
|
+
value: text,
|
|
764
|
+
onChange: (event) => setValue(field, event.target.value)
|
|
765
|
+
})]
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
function useContentValue(key) {
|
|
769
|
+
return useContent().values[key];
|
|
770
|
+
}
|
|
771
|
+
//#endregion
|
|
772
|
+
//#region src/content/image-processing.ts
|
|
773
|
+
const loadImage = (file) => new Promise((resolve, reject) => {
|
|
774
|
+
const url = URL.createObjectURL(file);
|
|
775
|
+
const image = new Image();
|
|
776
|
+
image.onload = () => {
|
|
777
|
+
URL.revokeObjectURL(url);
|
|
778
|
+
resolve({
|
|
779
|
+
image,
|
|
780
|
+
dimensions: {
|
|
781
|
+
width: image.naturalWidth,
|
|
782
|
+
height: image.naturalHeight
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
};
|
|
786
|
+
image.onerror = () => {
|
|
787
|
+
URL.revokeObjectURL(url);
|
|
788
|
+
reject(/* @__PURE__ */ new Error("Image could not be decoded."));
|
|
789
|
+
};
|
|
790
|
+
image.src = url;
|
|
791
|
+
});
|
|
792
|
+
const readImageDimensions = async (file) => (await loadImage(file)).dimensions;
|
|
793
|
+
const outputFileName = (name, mimeType) => {
|
|
794
|
+
const extension = mimeType === "image/png" ? "png" : mimeType === "image/jpeg" ? "jpg" : "webp";
|
|
795
|
+
return `${name.replace(/\.[^.]+$/, "") || "image"}.${extension}`;
|
|
796
|
+
};
|
|
797
|
+
const processImageFile = async (file, compression, cropOptions, cropState = {
|
|
798
|
+
x: 0,
|
|
799
|
+
y: 0,
|
|
800
|
+
zoom: 1
|
|
801
|
+
}) => {
|
|
802
|
+
const { image, dimensions } = await loadImage(file);
|
|
803
|
+
const source = cropOptions ? imageCropRectangle$1(dimensions, cropOptions.aspectRatio, cropState) : {
|
|
804
|
+
x: 0,
|
|
805
|
+
y: 0,
|
|
806
|
+
...dimensions
|
|
807
|
+
};
|
|
808
|
+
const requested = cropOptions ? cropOutputDimensions$1({
|
|
809
|
+
width: source.width,
|
|
810
|
+
height: source.height
|
|
811
|
+
}, cropOptions, compression) : containedImageDimensions$1({
|
|
812
|
+
width: source.width,
|
|
813
|
+
height: source.height
|
|
814
|
+
}, compression);
|
|
815
|
+
const canvas = document.createElement("canvas");
|
|
816
|
+
canvas.width = requested.width;
|
|
817
|
+
canvas.height = requested.height;
|
|
818
|
+
const context = canvas.getContext("2d");
|
|
819
|
+
if (!context) throw new Error("Canvas is unavailable.");
|
|
820
|
+
context.drawImage(image, source.x, source.y, source.width, source.height, 0, 0, requested.width, requested.height);
|
|
821
|
+
const mimeType = compression.mimeType ?? "image/webp";
|
|
822
|
+
const quality = typeof compression.quality === "number" && Number.isFinite(compression.quality) ? Math.max(.1, Math.min(1, compression.quality)) : .84;
|
|
823
|
+
const blob = await new Promise((resolve, reject) => canvas.toBlob((value) => value ? resolve(value) : reject(/* @__PURE__ */ new Error("Image processing failed.")), mimeType, quality));
|
|
824
|
+
const outputType = blob.type || mimeType;
|
|
825
|
+
return new File([blob], outputFileName(file.name, outputType), {
|
|
826
|
+
type: outputType,
|
|
827
|
+
lastModified: Date.now()
|
|
828
|
+
});
|
|
829
|
+
};
|
|
830
|
+
//#endregion
|
|
831
|
+
//#region src/content/editable-image.tsx
|
|
832
|
+
const DEFAULT_MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
|
833
|
+
const DEFAULT_MAX_SOURCE_BYTES = 40 * 1024 * 1024;
|
|
834
|
+
const DEFAULT_PROCESSING = {
|
|
835
|
+
compression: {
|
|
836
|
+
maxWidth: 2560,
|
|
837
|
+
maxHeight: 2560,
|
|
838
|
+
mimeType: "image/webp",
|
|
839
|
+
quality: .84
|
|
840
|
+
},
|
|
841
|
+
dimensions: { maxPixels: 5e7 }
|
|
842
|
+
};
|
|
843
|
+
const FOCUSABLE_SELECTOR = "button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
|
|
844
|
+
const focusableElements = (container) => Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
|
|
845
|
+
const validateImageFile = (file, maxUploadBytes, labels) => {
|
|
846
|
+
if (!file.type.startsWith("image/")) return labels.invalidType;
|
|
847
|
+
if (file.size > maxUploadBytes) return labels.tooLarge;
|
|
848
|
+
return null;
|
|
849
|
+
};
|
|
850
|
+
const compressionFor = (file, compression) => {
|
|
851
|
+
if (compression !== false) return compression ?? DEFAULT_PROCESSING.compression;
|
|
852
|
+
return { mimeType: file.type === "image/png" || file.type === "image/jpeg" || file.type === "image/webp" ? file.type : "image/webp" };
|
|
853
|
+
};
|
|
854
|
+
const preservesOriginalFormat = (file) => file.type === "image/gif" || file.type === "image/svg+xml";
|
|
855
|
+
const processingDimensionError = (dimensions, processing) => {
|
|
856
|
+
if (processing.dimensions === false) return null;
|
|
857
|
+
return validateImageDimensions(dimensions, {
|
|
858
|
+
...DEFAULT_PROCESSING.dimensions,
|
|
859
|
+
...processing.dimensions
|
|
860
|
+
});
|
|
861
|
+
};
|
|
862
|
+
function EditableImage({ field, alt = "", className, inputClassName, placeholder = "Image URL", onSelect, label, labels: labelOverrides, maxUploadBytes = DEFAULT_MAX_UPLOAD_BYTES, maxSourceBytes = DEFAULT_MAX_SOURCE_BYTES, processing = DEFAULT_PROCESSING }) {
|
|
863
|
+
const { values, isEditing, canEdit, media, setValue, teamSlug, imagePickerLabels: defaultLabels } = useContent();
|
|
864
|
+
const labels = {
|
|
865
|
+
...defaultLabels,
|
|
866
|
+
...labelOverrides
|
|
867
|
+
};
|
|
868
|
+
const fieldLabel = label ?? labels.field;
|
|
869
|
+
const source = typeof values[field] === "string" ? values[field] : "";
|
|
870
|
+
const [isPickerOpen, setPickerOpen] = useState(false);
|
|
871
|
+
const [images, setImages] = useState([]);
|
|
872
|
+
const [isLoading, setLoading] = useState(false);
|
|
873
|
+
const [operation, setOperation] = useState(null);
|
|
874
|
+
const [error, setError] = useState(null);
|
|
875
|
+
const [canRetry, setCanRetry] = useState(false);
|
|
876
|
+
const [pendingImage, setPendingImage] = useState(null);
|
|
877
|
+
const [crop, setCrop] = useState({
|
|
878
|
+
x: 0,
|
|
879
|
+
y: 0,
|
|
880
|
+
zoom: 1
|
|
881
|
+
});
|
|
882
|
+
const loadRequest = useRef(0);
|
|
883
|
+
const uploadInProgress = useRef(false);
|
|
884
|
+
useEffect(() => () => {
|
|
885
|
+
if (pendingImage) URL.revokeObjectURL(pendingImage.previewUrl);
|
|
886
|
+
}, [pendingImage?.previewUrl]);
|
|
887
|
+
const loadImages = async () => {
|
|
888
|
+
if (!media || !teamSlug) return;
|
|
889
|
+
const request = ++loadRequest.current;
|
|
890
|
+
setLoading(true);
|
|
891
|
+
setError(null);
|
|
892
|
+
setCanRetry(false);
|
|
893
|
+
try {
|
|
894
|
+
const result = await media.list(teamSlug, { mime: "image/" });
|
|
895
|
+
if (request !== loadRequest.current) return;
|
|
896
|
+
if (!result.ok) {
|
|
897
|
+
setError(labels.loadError);
|
|
898
|
+
setCanRetry(true);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
setImages(result.data ?? []);
|
|
902
|
+
} catch {
|
|
903
|
+
if (request === loadRequest.current) {
|
|
904
|
+
setError(labels.loadError);
|
|
905
|
+
setCanRetry(true);
|
|
906
|
+
}
|
|
907
|
+
} finally {
|
|
908
|
+
if (request === loadRequest.current) setLoading(false);
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
useEffect(() => {
|
|
912
|
+
if (!isPickerOpen || !media || !teamSlug) return;
|
|
913
|
+
loadImages();
|
|
914
|
+
return () => {
|
|
915
|
+
loadRequest.current += 1;
|
|
916
|
+
};
|
|
917
|
+
}, [isPickerOpen, teamSlug]);
|
|
918
|
+
const uploadFile = async (file) => {
|
|
919
|
+
if (!media || !teamSlug || uploadInProgress.current) return;
|
|
920
|
+
uploadInProgress.current = true;
|
|
921
|
+
setOperation("uploading");
|
|
922
|
+
try {
|
|
923
|
+
const result = await media.upload({
|
|
924
|
+
team: teamSlug,
|
|
925
|
+
file,
|
|
926
|
+
type: "output",
|
|
927
|
+
label: file.name
|
|
928
|
+
});
|
|
929
|
+
if (!result.ok || !result.data) {
|
|
930
|
+
setError(labels.uploadError);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
setImages((current) => [result.data, ...current]);
|
|
934
|
+
setValue(field, result.data.url);
|
|
935
|
+
setPickerOpen(false);
|
|
936
|
+
} catch {
|
|
937
|
+
setError(labels.uploadError);
|
|
938
|
+
} finally {
|
|
939
|
+
uploadInProgress.current = false;
|
|
940
|
+
setOperation(null);
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
const prepareUpload = async (file) => {
|
|
944
|
+
if (uploadInProgress.current) return;
|
|
945
|
+
setError(null);
|
|
946
|
+
setCanRetry(false);
|
|
947
|
+
const validationError = validateImageFile(file, maxSourceBytes, labels);
|
|
948
|
+
if (validationError) {
|
|
949
|
+
setError(validationError);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
uploadInProgress.current = true;
|
|
953
|
+
setOperation("processing");
|
|
954
|
+
try {
|
|
955
|
+
if (processing === false) {
|
|
956
|
+
if (file.size > maxUploadBytes) setError(labels.tooLarge);
|
|
957
|
+
else {
|
|
958
|
+
uploadInProgress.current = false;
|
|
959
|
+
setOperation(null);
|
|
960
|
+
await uploadFile(file);
|
|
961
|
+
}
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
const dimensions = await readImageDimensions(file);
|
|
965
|
+
const dimensionError = processingDimensionError(dimensions, processing);
|
|
966
|
+
if (dimensionError) {
|
|
967
|
+
setError(dimensionError === "too-small" ? labels.dimensionsTooSmall : labels.dimensionsTooLarge);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (processing.crop) {
|
|
971
|
+
if (!Number.isFinite(processing.crop.aspectRatio) || processing.crop.aspectRatio <= 0) throw new RangeError("Crop aspect ratio must be a positive finite number.");
|
|
972
|
+
setCrop({
|
|
973
|
+
x: 0,
|
|
974
|
+
y: 0,
|
|
975
|
+
zoom: 1
|
|
976
|
+
});
|
|
977
|
+
setPendingImage({
|
|
978
|
+
file,
|
|
979
|
+
dimensions,
|
|
980
|
+
previewUrl: URL.createObjectURL(file)
|
|
981
|
+
});
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
if (processing.compression === false || preservesOriginalFormat(file)) {
|
|
985
|
+
if (file.size > maxUploadBytes) {
|
|
986
|
+
setError(labels.tooLarge);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
uploadInProgress.current = false;
|
|
990
|
+
setOperation(null);
|
|
991
|
+
await uploadFile(file);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const compression = {
|
|
995
|
+
...DEFAULT_PROCESSING.compression,
|
|
996
|
+
...processing.compression
|
|
997
|
+
};
|
|
998
|
+
const outputDimensionError = processingDimensionError(containedImageDimensions(dimensions, compression), processing);
|
|
999
|
+
if (outputDimensionError) {
|
|
1000
|
+
setError(outputDimensionError === "too-small" ? labels.dimensionsTooSmall : labels.dimensionsTooLarge);
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
const output = await processImageFile(file, compression);
|
|
1004
|
+
if (output.size > maxUploadBytes) {
|
|
1005
|
+
setError(labels.tooLarge);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
uploadInProgress.current = false;
|
|
1009
|
+
setOperation(null);
|
|
1010
|
+
await uploadFile(output);
|
|
1011
|
+
} catch {
|
|
1012
|
+
setError(labels.uploadError);
|
|
1013
|
+
} finally {
|
|
1014
|
+
if (uploadInProgress.current) {
|
|
1015
|
+
uploadInProgress.current = false;
|
|
1016
|
+
setOperation(null);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
const applyCrop = async () => {
|
|
1021
|
+
if (!pendingImage || processing === false || !processing.crop || uploadInProgress.current) return;
|
|
1022
|
+
uploadInProgress.current = true;
|
|
1023
|
+
setOperation("processing");
|
|
1024
|
+
setError(null);
|
|
1025
|
+
try {
|
|
1026
|
+
const compression = {
|
|
1027
|
+
...DEFAULT_PROCESSING.compression,
|
|
1028
|
+
...processing.compression === false ? compressionFor(pendingImage.file, false) : processing.compression
|
|
1029
|
+
};
|
|
1030
|
+
const source = imageCropRectangle(pendingImage.dimensions, processing.crop.aspectRatio, crop);
|
|
1031
|
+
const outputDimensionError = processingDimensionError(cropOutputDimensions({
|
|
1032
|
+
width: source.width,
|
|
1033
|
+
height: source.height
|
|
1034
|
+
}, processing.crop, compression), processing);
|
|
1035
|
+
if (outputDimensionError) {
|
|
1036
|
+
setError(outputDimensionError === "too-small" ? labels.dimensionsTooSmall : labels.dimensionsTooLarge);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
const output = await processImageFile(pendingImage.file, compression, processing.crop, crop);
|
|
1040
|
+
if (output.size > maxUploadBytes) {
|
|
1041
|
+
setError(labels.tooLarge);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
setPendingImage(null);
|
|
1045
|
+
uploadInProgress.current = false;
|
|
1046
|
+
setOperation(null);
|
|
1047
|
+
await uploadFile(output);
|
|
1048
|
+
} catch {
|
|
1049
|
+
setError(labels.uploadError);
|
|
1050
|
+
} finally {
|
|
1051
|
+
if (uploadInProgress.current) {
|
|
1052
|
+
uploadInProgress.current = false;
|
|
1053
|
+
setOperation(null);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
if (!canEdit || !isEditing) return source ? /* @__PURE__ */ jsx("img", {
|
|
1058
|
+
src: source,
|
|
1059
|
+
alt,
|
|
1060
|
+
className
|
|
1061
|
+
}) : null;
|
|
1062
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1063
|
+
className: `group/content-field relative rounded-lg border border-dashed border-blue-400/80 bg-blue-50/30 p-2 transition hover:border-blue-500 hover:bg-blue-50/60 focus-within:border-blue-600 focus-within:ring-2 focus-within:ring-blue-500/30 ${className ?? ""}`,
|
|
1064
|
+
"data-content-editable": field,
|
|
1065
|
+
children: [
|
|
1066
|
+
/* @__PURE__ */ jsx("span", {
|
|
1067
|
+
className: "pointer-events-none absolute -top-2 left-2 z-10 rounded bg-blue-600 px-1.5 py-0.5 text-[10px] font-semibold uppercase leading-none tracking-wide text-white shadow-sm",
|
|
1068
|
+
children: fieldLabel
|
|
1069
|
+
}),
|
|
1070
|
+
source && /* @__PURE__ */ jsx("img", {
|
|
1071
|
+
src: source,
|
|
1072
|
+
alt,
|
|
1073
|
+
className: "h-auto w-full rounded-md"
|
|
1074
|
+
}),
|
|
1075
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1076
|
+
className: "mt-2 flex flex-wrap items-center gap-2",
|
|
1077
|
+
children: [
|
|
1078
|
+
onSelect && /* @__PURE__ */ jsx("button", {
|
|
1079
|
+
className: "rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2",
|
|
1080
|
+
type: "button",
|
|
1081
|
+
onClick: onSelect,
|
|
1082
|
+
children: labels.choose
|
|
1083
|
+
}),
|
|
1084
|
+
!onSelect && media && teamSlug && /* @__PURE__ */ jsx("button", {
|
|
1085
|
+
className: "rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2",
|
|
1086
|
+
type: "button",
|
|
1087
|
+
onClick: () => setPickerOpen(true),
|
|
1088
|
+
children: labels.choose
|
|
1089
|
+
}),
|
|
1090
|
+
/* @__PURE__ */ jsx("input", {
|
|
1091
|
+
"aria-label": fieldLabel,
|
|
1092
|
+
className: `${inputClassName ?? ""} min-w-0 flex-1 rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 outline-none transition hover:border-blue-400 focus:border-blue-600 focus:ring-2 focus:ring-blue-500/30`,
|
|
1093
|
+
type: "url",
|
|
1094
|
+
placeholder,
|
|
1095
|
+
value: source,
|
|
1096
|
+
onChange: (event) => setValue(field, event.target.value)
|
|
1097
|
+
})
|
|
1098
|
+
]
|
|
1099
|
+
}),
|
|
1100
|
+
/* @__PURE__ */ jsx(ImagePicker, {
|
|
1101
|
+
open: isPickerOpen,
|
|
1102
|
+
onClose: () => {
|
|
1103
|
+
if (!uploadInProgress.current) {
|
|
1104
|
+
setPendingImage(null);
|
|
1105
|
+
setPickerOpen(false);
|
|
1106
|
+
}
|
|
1107
|
+
},
|
|
1108
|
+
images,
|
|
1109
|
+
isLoading,
|
|
1110
|
+
operation,
|
|
1111
|
+
error,
|
|
1112
|
+
canRetry,
|
|
1113
|
+
labels,
|
|
1114
|
+
pendingImage,
|
|
1115
|
+
crop,
|
|
1116
|
+
cropAspectRatio: processing === false ? void 0 : processing.crop?.aspectRatio,
|
|
1117
|
+
onCropChange: setCrop,
|
|
1118
|
+
onApplyCrop: applyCrop,
|
|
1119
|
+
onBack: () => setPendingImage(null),
|
|
1120
|
+
onRetry: loadImages,
|
|
1121
|
+
onUpload: prepareUpload,
|
|
1122
|
+
onSelect: (url) => {
|
|
1123
|
+
if (uploadInProgress.current) return;
|
|
1124
|
+
setValue(field, url);
|
|
1125
|
+
setPickerOpen(false);
|
|
1126
|
+
}
|
|
1127
|
+
})
|
|
1128
|
+
]
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
function ImagePicker({ open, onClose, images, isLoading, operation, error, canRetry, labels, pendingImage, crop, cropAspectRatio, onCropChange, onApplyCrop, onBack, onRetry, onUpload, onSelect }) {
|
|
1132
|
+
const dialogRef = useRef(null);
|
|
1133
|
+
const closeButtonRef = useRef(null);
|
|
1134
|
+
const previousFocusRef = useRef(null);
|
|
1135
|
+
const [isDragging, setDragging] = useState(false);
|
|
1136
|
+
const titleId = useId();
|
|
1137
|
+
const descriptionId = useId();
|
|
1138
|
+
const isBusy = operation !== null;
|
|
1139
|
+
useEffect(() => {
|
|
1140
|
+
if (!open) return;
|
|
1141
|
+
previousFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
1142
|
+
const previousOverflow = document.body.style.overflow;
|
|
1143
|
+
document.body.style.overflow = "hidden";
|
|
1144
|
+
const frame = window.requestAnimationFrame(() => closeButtonRef.current?.focus());
|
|
1145
|
+
const keydown = (event) => {
|
|
1146
|
+
if (event.key === "Escape") {
|
|
1147
|
+
event.preventDefault();
|
|
1148
|
+
if (!isBusy) onClose();
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
if (event.key !== "Tab" || !dialogRef.current) return;
|
|
1152
|
+
const focusable = focusableElements(dialogRef.current);
|
|
1153
|
+
if (focusable.length === 0) return;
|
|
1154
|
+
const first = focusable[0];
|
|
1155
|
+
const last = focusable.at(-1);
|
|
1156
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
1157
|
+
event.preventDefault();
|
|
1158
|
+
last.focus();
|
|
1159
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
1160
|
+
event.preventDefault();
|
|
1161
|
+
first.focus();
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
document.addEventListener("keydown", keydown);
|
|
1165
|
+
return () => {
|
|
1166
|
+
window.cancelAnimationFrame(frame);
|
|
1167
|
+
document.removeEventListener("keydown", keydown);
|
|
1168
|
+
document.body.style.overflow = previousOverflow;
|
|
1169
|
+
previousFocusRef.current?.focus();
|
|
1170
|
+
};
|
|
1171
|
+
}, [open]);
|
|
1172
|
+
if (!open) return null;
|
|
1173
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1174
|
+
className: "fixed inset-0 z-[1100] grid place-items-center bg-slate-950/55 p-4",
|
|
1175
|
+
role: "presentation",
|
|
1176
|
+
onMouseDown: (event) => {
|
|
1177
|
+
if (event.target === event.currentTarget) onClose();
|
|
1178
|
+
},
|
|
1179
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1180
|
+
ref: dialogRef,
|
|
1181
|
+
className: "max-h-[min(44rem,calc(100vh-2rem))] w-full max-w-2xl overflow-y-auto rounded-xl bg-white p-5 shadow-2xl",
|
|
1182
|
+
role: "dialog",
|
|
1183
|
+
"aria-modal": "true",
|
|
1184
|
+
"aria-labelledby": titleId,
|
|
1185
|
+
"aria-describedby": descriptionId,
|
|
1186
|
+
children: [
|
|
1187
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1188
|
+
className: "flex items-start justify-between gap-4",
|
|
1189
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", {
|
|
1190
|
+
id: titleId,
|
|
1191
|
+
className: "text-lg font-semibold text-slate-950",
|
|
1192
|
+
children: pendingImage ? labels.cropTitle : labels.libraryTitle
|
|
1193
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1194
|
+
id: descriptionId,
|
|
1195
|
+
className: "mt-1 text-sm text-slate-500",
|
|
1196
|
+
children: pendingImage ? labels.cropDescription : labels.libraryDescription
|
|
1197
|
+
})] }), /* @__PURE__ */ jsx("button", {
|
|
1198
|
+
ref: closeButtonRef,
|
|
1199
|
+
"aria-label": labels.close,
|
|
1200
|
+
className: "rounded-lg px-3 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-50",
|
|
1201
|
+
type: "button",
|
|
1202
|
+
disabled: isBusy,
|
|
1203
|
+
onClick: onClose,
|
|
1204
|
+
children: labels.close
|
|
1205
|
+
})]
|
|
1206
|
+
}),
|
|
1207
|
+
error && /* @__PURE__ */ jsxs("div", {
|
|
1208
|
+
className: "mt-4 flex items-center justify-between gap-3 rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700",
|
|
1209
|
+
role: "alert",
|
|
1210
|
+
children: [/* @__PURE__ */ jsx("span", { children: error }), canRetry && !isBusy && /* @__PURE__ */ jsx("button", {
|
|
1211
|
+
className: "shrink-0 rounded px-2 py-1 font-semibold hover:bg-red-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500",
|
|
1212
|
+
type: "button",
|
|
1213
|
+
onClick: () => void onRetry(),
|
|
1214
|
+
children: labels.retry
|
|
1215
|
+
})]
|
|
1216
|
+
}),
|
|
1217
|
+
pendingImage && cropAspectRatio ? /* @__PURE__ */ jsx(ImageCropEditor, {
|
|
1218
|
+
pendingImage,
|
|
1219
|
+
aspectRatio: cropAspectRatio,
|
|
1220
|
+
crop,
|
|
1221
|
+
isProcessing: isBusy,
|
|
1222
|
+
labels,
|
|
1223
|
+
onChange: onCropChange,
|
|
1224
|
+
onApply: onApplyCrop,
|
|
1225
|
+
onBack
|
|
1226
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1227
|
+
/* @__PURE__ */ jsxs("label", {
|
|
1228
|
+
className: `mt-5 flex flex-col items-center justify-center rounded-lg border border-dashed px-4 py-7 text-center text-sm font-medium transition focus-within:ring-2 focus-within:ring-blue-500 ${isBusy ? "cursor-wait border-slate-200 bg-slate-50 text-slate-400" : isDragging ? "cursor-copy border-blue-600 bg-blue-50 text-blue-700" : "cursor-pointer border-slate-300 text-slate-600 hover:border-blue-400 hover:text-blue-600"}`,
|
|
1229
|
+
onDragEnter: (event) => {
|
|
1230
|
+
event.preventDefault();
|
|
1231
|
+
if (!isBusy) setDragging(true);
|
|
1232
|
+
},
|
|
1233
|
+
onDragOver: (event) => {
|
|
1234
|
+
event.preventDefault();
|
|
1235
|
+
if (!isBusy) setDragging(true);
|
|
1236
|
+
},
|
|
1237
|
+
onDragLeave: (event) => {
|
|
1238
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
1239
|
+
},
|
|
1240
|
+
onDrop: (event) => {
|
|
1241
|
+
event.preventDefault();
|
|
1242
|
+
setDragging(false);
|
|
1243
|
+
if (isBusy) return;
|
|
1244
|
+
const file = event.dataTransfer.files?.[0];
|
|
1245
|
+
if (file) onUpload(file);
|
|
1246
|
+
},
|
|
1247
|
+
children: [
|
|
1248
|
+
/* @__PURE__ */ jsx("input", {
|
|
1249
|
+
className: "sr-only",
|
|
1250
|
+
type: "file",
|
|
1251
|
+
accept: "image/*",
|
|
1252
|
+
disabled: isBusy,
|
|
1253
|
+
onChange: (event) => {
|
|
1254
|
+
const file = event.target.files?.[0];
|
|
1255
|
+
if (file) onUpload(file);
|
|
1256
|
+
event.currentTarget.value = "";
|
|
1257
|
+
}
|
|
1258
|
+
}),
|
|
1259
|
+
/* @__PURE__ */ jsx("span", { children: isDragging ? labels.drop : labels.upload }),
|
|
1260
|
+
operation && /* @__PURE__ */ jsx("span", {
|
|
1261
|
+
className: "mt-2 text-xs font-normal text-slate-500",
|
|
1262
|
+
role: "status",
|
|
1263
|
+
children: operation === "processing" ? labels.processing : labels.uploading
|
|
1264
|
+
})
|
|
1265
|
+
]
|
|
1266
|
+
}),
|
|
1267
|
+
isLoading && /* @__PURE__ */ jsx("div", {
|
|
1268
|
+
className: "mt-5 rounded-lg bg-slate-50 p-6 text-center text-sm text-slate-500",
|
|
1269
|
+
role: "status",
|
|
1270
|
+
children: labels.loading
|
|
1271
|
+
}),
|
|
1272
|
+
!isLoading && !error && images.length === 0 && /* @__PURE__ */ jsx("div", {
|
|
1273
|
+
className: "mt-5 rounded-lg bg-slate-50 p-6 text-center text-sm text-slate-500",
|
|
1274
|
+
children: labels.empty
|
|
1275
|
+
}),
|
|
1276
|
+
!isLoading && images.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
1277
|
+
className: "mt-5 grid grid-cols-2 gap-3 sm:grid-cols-3",
|
|
1278
|
+
role: "list",
|
|
1279
|
+
children: images.map((image) => /* @__PURE__ */ jsxs("button", {
|
|
1280
|
+
className: "overflow-hidden rounded-lg border border-slate-200 text-left transition hover:border-blue-400 hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-50",
|
|
1281
|
+
type: "button",
|
|
1282
|
+
disabled: isBusy,
|
|
1283
|
+
onClick: () => onSelect(image.url),
|
|
1284
|
+
children: [/* @__PURE__ */ jsx("img", {
|
|
1285
|
+
className: "aspect-square w-full object-cover",
|
|
1286
|
+
src: image.url,
|
|
1287
|
+
alt: image.name
|
|
1288
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1289
|
+
className: "block truncate p-2 text-xs text-slate-700",
|
|
1290
|
+
children: image.name
|
|
1291
|
+
})]
|
|
1292
|
+
}, image.id))
|
|
1293
|
+
})
|
|
1294
|
+
] })
|
|
1295
|
+
]
|
|
1296
|
+
})
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
function ImageCropEditor({ pendingImage, aspectRatio, crop, isProcessing, labels, onChange, onApply, onBack }) {
|
|
1300
|
+
const previewRef = useRef(null);
|
|
1301
|
+
const drag = useRef(null);
|
|
1302
|
+
const control = (key, value) => onChange({
|
|
1303
|
+
...crop,
|
|
1304
|
+
[key]: value
|
|
1305
|
+
});
|
|
1306
|
+
const clampedX = Math.max(-1, Math.min(1, crop.x));
|
|
1307
|
+
const clampedY = Math.max(-1, Math.min(1, crop.y));
|
|
1308
|
+
const clampedZoom = Math.max(1, crop.zoom);
|
|
1309
|
+
const sourceAspect = pendingImage.dimensions.width / pendingImage.dimensions.height;
|
|
1310
|
+
const baseWidth = sourceAspect > aspectRatio ? sourceAspect / aspectRatio * 100 : 100;
|
|
1311
|
+
const baseHeight = sourceAspect > aspectRatio ? 100 : aspectRatio / sourceAspect * 100;
|
|
1312
|
+
const width = baseWidth * clampedZoom;
|
|
1313
|
+
const height = baseHeight * clampedZoom;
|
|
1314
|
+
const left = -(width - 100) * ((clampedX + 1) / 2);
|
|
1315
|
+
const top = -(height - 100) * ((clampedY + 1) / 2);
|
|
1316
|
+
const moveCrop = (clientX, clientY) => {
|
|
1317
|
+
const start = drag.current;
|
|
1318
|
+
const bounds = previewRef.current?.getBoundingClientRect();
|
|
1319
|
+
if (!start || !bounds) return;
|
|
1320
|
+
const overflowX = bounds.width * ((width - 100) / 100);
|
|
1321
|
+
const overflowY = bounds.height * ((height - 100) / 100);
|
|
1322
|
+
const x = overflowX > 0 ? start.crop.x - 2 * (clientX - start.x) / overflowX : start.crop.x;
|
|
1323
|
+
const y = overflowY > 0 ? start.crop.y - 2 * (clientY - start.y) / overflowY : start.crop.y;
|
|
1324
|
+
onChange({
|
|
1325
|
+
...crop,
|
|
1326
|
+
x: Math.max(-1, Math.min(1, x)),
|
|
1327
|
+
y: Math.max(-1, Math.min(1, y))
|
|
1328
|
+
});
|
|
1329
|
+
};
|
|
1330
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1331
|
+
className: "mt-5",
|
|
1332
|
+
children: [
|
|
1333
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1334
|
+
ref: previewRef,
|
|
1335
|
+
className: `relative mx-auto w-full max-w-xl touch-none overflow-hidden rounded-lg bg-slate-950 ${isProcessing ? "cursor-wait" : "cursor-grab active:cursor-grabbing"}`,
|
|
1336
|
+
style: { aspectRatio },
|
|
1337
|
+
onPointerDown: (event) => {
|
|
1338
|
+
if (isProcessing) return;
|
|
1339
|
+
drag.current = {
|
|
1340
|
+
pointerId: event.pointerId,
|
|
1341
|
+
x: event.clientX,
|
|
1342
|
+
y: event.clientY,
|
|
1343
|
+
crop
|
|
1344
|
+
};
|
|
1345
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
1346
|
+
},
|
|
1347
|
+
onPointerMove: (event) => {
|
|
1348
|
+
if (drag.current?.pointerId === event.pointerId) moveCrop(event.clientX, event.clientY);
|
|
1349
|
+
},
|
|
1350
|
+
onPointerUp: (event) => {
|
|
1351
|
+
if (drag.current?.pointerId !== event.pointerId) return;
|
|
1352
|
+
drag.current = null;
|
|
1353
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
1354
|
+
},
|
|
1355
|
+
onPointerCancel: () => {
|
|
1356
|
+
drag.current = null;
|
|
1357
|
+
},
|
|
1358
|
+
children: [/* @__PURE__ */ jsx("img", {
|
|
1359
|
+
src: pendingImage.previewUrl,
|
|
1360
|
+
alt: "",
|
|
1361
|
+
className: "absolute max-w-none select-none",
|
|
1362
|
+
draggable: false,
|
|
1363
|
+
style: {
|
|
1364
|
+
width: `${width}%`,
|
|
1365
|
+
height: `${height}%`,
|
|
1366
|
+
left: `${left}%`,
|
|
1367
|
+
top: `${top}%`
|
|
1368
|
+
}
|
|
1369
|
+
}), /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-0 ring-1 ring-inset ring-white/70" })]
|
|
1370
|
+
}),
|
|
1371
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1372
|
+
className: "mt-5 grid gap-4 sm:grid-cols-3",
|
|
1373
|
+
children: [
|
|
1374
|
+
/* @__PURE__ */ jsxs("label", {
|
|
1375
|
+
className: "grid gap-1 text-xs font-medium text-slate-700",
|
|
1376
|
+
children: [/* @__PURE__ */ jsx("span", { children: labels.zoom }), /* @__PURE__ */ jsx("input", {
|
|
1377
|
+
"aria-label": labels.zoom,
|
|
1378
|
+
type: "range",
|
|
1379
|
+
min: "1",
|
|
1380
|
+
max: "3",
|
|
1381
|
+
step: "0.01",
|
|
1382
|
+
value: crop.zoom,
|
|
1383
|
+
disabled: isProcessing,
|
|
1384
|
+
onChange: (event) => control("zoom", Number(event.target.value))
|
|
1385
|
+
})]
|
|
1386
|
+
}),
|
|
1387
|
+
/* @__PURE__ */ jsxs("label", {
|
|
1388
|
+
className: "grid gap-1 text-xs font-medium text-slate-700",
|
|
1389
|
+
children: [/* @__PURE__ */ jsx("span", { children: labels.positionHorizontal }), /* @__PURE__ */ jsx("input", {
|
|
1390
|
+
"aria-label": labels.positionHorizontal,
|
|
1391
|
+
type: "range",
|
|
1392
|
+
min: "-1",
|
|
1393
|
+
max: "1",
|
|
1394
|
+
step: "0.01",
|
|
1395
|
+
value: crop.x,
|
|
1396
|
+
disabled: isProcessing,
|
|
1397
|
+
onChange: (event) => control("x", Number(event.target.value))
|
|
1398
|
+
})]
|
|
1399
|
+
}),
|
|
1400
|
+
/* @__PURE__ */ jsxs("label", {
|
|
1401
|
+
className: "grid gap-1 text-xs font-medium text-slate-700",
|
|
1402
|
+
children: [/* @__PURE__ */ jsx("span", { children: labels.positionVertical }), /* @__PURE__ */ jsx("input", {
|
|
1403
|
+
"aria-label": labels.positionVertical,
|
|
1404
|
+
type: "range",
|
|
1405
|
+
min: "-1",
|
|
1406
|
+
max: "1",
|
|
1407
|
+
step: "0.01",
|
|
1408
|
+
value: crop.y,
|
|
1409
|
+
disabled: isProcessing,
|
|
1410
|
+
onChange: (event) => control("y", Number(event.target.value))
|
|
1411
|
+
})]
|
|
1412
|
+
})
|
|
1413
|
+
]
|
|
1414
|
+
}),
|
|
1415
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1416
|
+
className: "mt-5 flex justify-end gap-2",
|
|
1417
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
1418
|
+
className: "rounded-lg px-3 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-100 disabled:opacity-50",
|
|
1419
|
+
type: "button",
|
|
1420
|
+
disabled: isProcessing,
|
|
1421
|
+
onClick: onBack,
|
|
1422
|
+
children: labels.back
|
|
1423
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1424
|
+
className: "rounded-lg bg-blue-600 px-3 py-2 text-sm font-semibold text-white hover:bg-blue-500 disabled:opacity-50",
|
|
1425
|
+
type: "button",
|
|
1426
|
+
disabled: isProcessing,
|
|
1427
|
+
onClick: () => void onApply(),
|
|
1428
|
+
children: isProcessing ? labels.processing : labels.applyCrop
|
|
1429
|
+
})]
|
|
1430
|
+
})
|
|
1431
|
+
]
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
//#endregion
|
|
1435
|
+
//#region src/content/toolbar.tsx
|
|
1436
|
+
function ContentToolbar({ className, labels = {}, confirmCancel, confirmPublish }) {
|
|
1437
|
+
const { canEdit, canPublish, isDirty, isEditing, isSaving, operation, error, message, cancel: cancelEditing, setEditing, save, publish } = useContent();
|
|
1438
|
+
if (!canEdit) return null;
|
|
1439
|
+
const edit = labels.edit ?? "Edit";
|
|
1440
|
+
const saveLabel = labels.save ?? "Save draft";
|
|
1441
|
+
const savingLabel = labels.saving ?? "Saving…";
|
|
1442
|
+
const publishLabel = labels.publish ?? "Publish";
|
|
1443
|
+
const publishingLabel = labels.publishing ?? "Publishing…";
|
|
1444
|
+
const cancel = labels.cancel ?? "Cancel";
|
|
1445
|
+
const toolbar = labels.toolbar ?? "Content editor";
|
|
1446
|
+
const unsaved = labels.unsaved ?? "Unsaved changes";
|
|
1447
|
+
const handlePublish = async () => {
|
|
1448
|
+
if (confirmPublish && !await confirmPublish()) return;
|
|
1449
|
+
await publish();
|
|
1450
|
+
};
|
|
1451
|
+
const handleCancel = async () => {
|
|
1452
|
+
if (isDirty && confirmCancel && !await confirmCancel()) return;
|
|
1453
|
+
cancelEditing();
|
|
1454
|
+
};
|
|
1455
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1456
|
+
className: `flex flex-wrap items-center gap-2 ${className ?? ""}`,
|
|
1457
|
+
role: "toolbar",
|
|
1458
|
+
"aria-label": toolbar,
|
|
1459
|
+
"aria-busy": isSaving,
|
|
1460
|
+
children: [
|
|
1461
|
+
!isEditing ? /* @__PURE__ */ jsx("button", {
|
|
1462
|
+
className: "rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-blue-500",
|
|
1463
|
+
type: "button",
|
|
1464
|
+
onClick: () => setEditing(true),
|
|
1465
|
+
children: edit
|
|
1466
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1467
|
+
/* @__PURE__ */ jsx("button", {
|
|
1468
|
+
className: "rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-blue-500 disabled:opacity-60",
|
|
1469
|
+
type: "button",
|
|
1470
|
+
disabled: isSaving || !isDirty,
|
|
1471
|
+
onClick: () => void save(),
|
|
1472
|
+
children: operation === "save" ? savingLabel : saveLabel
|
|
1473
|
+
}),
|
|
1474
|
+
canPublish && /* @__PURE__ */ jsx("button", {
|
|
1475
|
+
className: "rounded-lg bg-emerald-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-emerald-500 disabled:opacity-60",
|
|
1476
|
+
type: "button",
|
|
1477
|
+
disabled: isSaving,
|
|
1478
|
+
onClick: () => void handlePublish(),
|
|
1479
|
+
children: operation === "publish" ? publishingLabel : publishLabel
|
|
1480
|
+
}),
|
|
1481
|
+
/* @__PURE__ */ jsx("button", {
|
|
1482
|
+
className: "rounded-lg px-3 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-100 disabled:opacity-60",
|
|
1483
|
+
type: "button",
|
|
1484
|
+
disabled: isSaving,
|
|
1485
|
+
onClick: () => void handleCancel(),
|
|
1486
|
+
children: cancel
|
|
1487
|
+
})
|
|
1488
|
+
] }),
|
|
1489
|
+
error && /* @__PURE__ */ jsx("span", {
|
|
1490
|
+
className: "w-full text-sm text-red-600 dark:text-red-400",
|
|
1491
|
+
role: "alert",
|
|
1492
|
+
children: error
|
|
1493
|
+
}),
|
|
1494
|
+
message && /* @__PURE__ */ jsx("span", {
|
|
1495
|
+
className: "w-full text-sm text-emerald-700",
|
|
1496
|
+
"aria-live": "polite",
|
|
1497
|
+
children: message
|
|
1498
|
+
}),
|
|
1499
|
+
isEditing && isDirty && /* @__PURE__ */ jsx("span", {
|
|
1500
|
+
className: "w-full text-xs text-slate-500",
|
|
1501
|
+
"aria-live": "polite",
|
|
1502
|
+
children: unsaved
|
|
1503
|
+
})
|
|
1504
|
+
]
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
//#endregion
|
|
1508
|
+
//#region src/content/content-editor.tsx
|
|
1509
|
+
function EditorPanel({ confirmCancel, confirmPublish, labels, title }) {
|
|
1510
|
+
const { canEdit } = useContent();
|
|
1511
|
+
if (!canEdit) return null;
|
|
1512
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1513
|
+
className: "fixed right-4 bottom-4 z-[1000] w-[min(24rem,calc(100vw-2rem))] rounded-xl border border-slate-200 bg-white p-3 text-slate-950 shadow-xl",
|
|
1514
|
+
children: [/* @__PURE__ */ jsx("strong", {
|
|
1515
|
+
className: "mb-2 block text-sm font-semibold",
|
|
1516
|
+
children: title
|
|
1517
|
+
}), /* @__PURE__ */ jsx(ContentToolbar, {
|
|
1518
|
+
confirmCancel,
|
|
1519
|
+
confirmPublish,
|
|
1520
|
+
labels
|
|
1521
|
+
})]
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
function ContentEditor({ children, confirmCancel, confirmPublish, labels, title = "Edit content", ...provider }) {
|
|
1525
|
+
return /* @__PURE__ */ jsx(ContentProvider, {
|
|
1526
|
+
...provider,
|
|
1527
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1528
|
+
"data-content-editor": "true",
|
|
1529
|
+
children: [/* @__PURE__ */ jsx(EditorPanel, {
|
|
1530
|
+
confirmCancel,
|
|
1531
|
+
confirmPublish,
|
|
1532
|
+
labels,
|
|
1533
|
+
title
|
|
1534
|
+
}), children]
|
|
1535
|
+
})
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
//#endregion
|
|
1539
|
+
//#region src/content/draft-store.ts
|
|
1540
|
+
const isDraft = (value) => {
|
|
1541
|
+
if (!value || typeof value !== "object") return false;
|
|
1542
|
+
const draft = value;
|
|
1543
|
+
if (draft.contentId !== void 0 && typeof draft.contentId !== "string") return false;
|
|
1544
|
+
return Boolean(draft.values && typeof draft.values === "object" && !Array.isArray(draft.values));
|
|
1545
|
+
};
|
|
1546
|
+
const createContentDraftStore = (storageKey, storage) => {
|
|
1547
|
+
const resolvedStorage = () => {
|
|
1548
|
+
if (storage) return storage;
|
|
1549
|
+
if (typeof window === "undefined") return null;
|
|
1550
|
+
try {
|
|
1551
|
+
return window.sessionStorage;
|
|
1552
|
+
} catch {
|
|
1553
|
+
return null;
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1556
|
+
return {
|
|
1557
|
+
get: () => {
|
|
1558
|
+
try {
|
|
1559
|
+
const serialized = resolvedStorage()?.getItem(storageKey);
|
|
1560
|
+
if (!serialized) return null;
|
|
1561
|
+
const value = JSON.parse(serialized);
|
|
1562
|
+
if (!isDraft(value)) return null;
|
|
1563
|
+
return {
|
|
1564
|
+
contentId: value.contentId,
|
|
1565
|
+
values: value.values
|
|
1566
|
+
};
|
|
1567
|
+
} catch {
|
|
1568
|
+
return null;
|
|
1569
|
+
}
|
|
1570
|
+
},
|
|
1571
|
+
set: (draft) => {
|
|
1572
|
+
try {
|
|
1573
|
+
const target = resolvedStorage();
|
|
1574
|
+
if (!target) return false;
|
|
1575
|
+
target.setItem(storageKey, JSON.stringify(draft));
|
|
1576
|
+
return true;
|
|
1577
|
+
} catch {
|
|
1578
|
+
return false;
|
|
1579
|
+
}
|
|
1580
|
+
},
|
|
1581
|
+
clear: () => {
|
|
1582
|
+
try {
|
|
1583
|
+
const target = resolvedStorage();
|
|
1584
|
+
if (!target) return false;
|
|
1585
|
+
target.removeItem(storageKey);
|
|
1586
|
+
return true;
|
|
1587
|
+
} catch {
|
|
1588
|
+
return false;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
};
|
|
1593
|
+
//#endregion
|
|
1594
|
+
//#region src/content/return-url.ts
|
|
1595
|
+
const getContentSessionReturnUrl = (currentUrl) => {
|
|
1596
|
+
const returnUrl = new URL(currentUrl);
|
|
1597
|
+
returnUrl.searchParams.set("platform", "app");
|
|
1598
|
+
return returnUrl.toString();
|
|
1599
|
+
};
|
|
1600
|
+
//#endregion
|
|
1601
|
+
//#region src/content/session-store.ts
|
|
1602
|
+
const createSessionTokenStore = (storageKey, storage) => {
|
|
1603
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1604
|
+
const resolvedStorage = () => {
|
|
1605
|
+
if (storage) return storage;
|
|
1606
|
+
if (typeof window === "undefined") return null;
|
|
1607
|
+
try {
|
|
1608
|
+
return window.localStorage;
|
|
1609
|
+
} catch {
|
|
1610
|
+
return null;
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
const emit = () => {
|
|
1614
|
+
for (const listener of listeners) listener();
|
|
1615
|
+
};
|
|
1616
|
+
const onStorage = (event) => {
|
|
1617
|
+
if (event.key === storageKey) emit();
|
|
1618
|
+
};
|
|
1619
|
+
return {
|
|
1620
|
+
getSnapshot: () => {
|
|
1621
|
+
try {
|
|
1622
|
+
return resolvedStorage()?.getItem(storageKey) ?? null;
|
|
1623
|
+
} catch {
|
|
1624
|
+
return null;
|
|
1625
|
+
}
|
|
1626
|
+
},
|
|
1627
|
+
getServerSnapshot: () => null,
|
|
1628
|
+
subscribe: (listener) => {
|
|
1629
|
+
listeners.add(listener);
|
|
1630
|
+
if (typeof window !== "undefined") window.addEventListener("storage", onStorage);
|
|
1631
|
+
return () => {
|
|
1632
|
+
listeners.delete(listener);
|
|
1633
|
+
if (listeners.size === 0 && typeof window !== "undefined") window.removeEventListener("storage", onStorage);
|
|
1634
|
+
};
|
|
1635
|
+
},
|
|
1636
|
+
set: (token) => {
|
|
1637
|
+
try {
|
|
1638
|
+
const target = resolvedStorage();
|
|
1639
|
+
if (!target) return false;
|
|
1640
|
+
target.setItem(storageKey, token);
|
|
1641
|
+
emit();
|
|
1642
|
+
return true;
|
|
1643
|
+
} catch {
|
|
1644
|
+
return false;
|
|
1645
|
+
}
|
|
1646
|
+
},
|
|
1647
|
+
clear: () => {
|
|
1648
|
+
try {
|
|
1649
|
+
const target = resolvedStorage();
|
|
1650
|
+
if (!target) return false;
|
|
1651
|
+
target.removeItem(storageKey);
|
|
1652
|
+
emit();
|
|
1653
|
+
return true;
|
|
1654
|
+
} catch {
|
|
1655
|
+
return false;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
};
|
|
1659
|
+
};
|
|
1660
|
+
//#endregion
|
|
1661
|
+
//#region src/content/use-content-session.ts
|
|
1662
|
+
const useContentSession = ({ env = "mainnet", exchange, sessionStore, sessionStorageKey, team }) => {
|
|
1663
|
+
const defaultStore = useMemo(() => createSessionTokenStore(sessionStorageKey), [sessionStorageKey]);
|
|
1664
|
+
const store = sessionStore ?? defaultStore;
|
|
1665
|
+
const storedToken = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot);
|
|
1666
|
+
const [handoffToken, setHandoffToken] = useState(null);
|
|
1667
|
+
const [error, setError] = useState(null);
|
|
1668
|
+
useEffect(() => {
|
|
1669
|
+
const handoff = getAuthFragmentToken(window.location.href);
|
|
1670
|
+
if (!handoff) return;
|
|
1671
|
+
window.history.replaceState(null, "", `${window.location.pathname}${window.location.search}`);
|
|
1672
|
+
const completeExchange = async () => {
|
|
1673
|
+
const token = await (exchange ? exchange(handoff) : createClient({
|
|
1674
|
+
env,
|
|
1675
|
+
team
|
|
1676
|
+
}).auth.exchangeNativeSession(handoff).then((response) => {
|
|
1677
|
+
if (!response.ok || !response.data?.token) throw new Error("Session exchange failed.");
|
|
1678
|
+
return response.data.token;
|
|
1679
|
+
}));
|
|
1680
|
+
if (!store.set(token)) throw new Error("Session storage is unavailable.");
|
|
1681
|
+
setHandoffToken(token);
|
|
1682
|
+
};
|
|
1683
|
+
completeExchange().catch(() => setError("The session could not be established."));
|
|
1684
|
+
}, [
|
|
1685
|
+
env,
|
|
1686
|
+
sessionStorageKey,
|
|
1687
|
+
team
|
|
1688
|
+
]);
|
|
1689
|
+
return {
|
|
1690
|
+
clear: useCallback(() => {
|
|
1691
|
+
setHandoffToken(null);
|
|
1692
|
+
store.clear();
|
|
1693
|
+
}, [sessionStorageKey]),
|
|
1694
|
+
error,
|
|
1695
|
+
token: handoffToken ?? storedToken
|
|
1696
|
+
};
|
|
1697
|
+
};
|
|
1698
|
+
//#endregion
|
|
1699
|
+
//#region src/content/managed-content-editor.tsx
|
|
1700
|
+
function ManagedContentEditor({ authErrorClassName, authErrorMessage = "The session could not be established.", children, config, confirmLeave, confirmCancel, confirmPublish, draftStore, imagePickerLabels, initialContent, labels, messages, reauthenticateLabel = "Sign in again", reauthenticateUrl, sessionStore, sessionExpiredMessage = "Your editing session expired. Sign in again to continue; your unsaved changes will be restored.", title = "Edit content" }) {
|
|
1701
|
+
const { contentId, env = "mainnet", publicSlug, sessionStorageKey, team } = config;
|
|
1702
|
+
const { error, token } = useContentSession({
|
|
1703
|
+
env,
|
|
1704
|
+
sessionStore,
|
|
1705
|
+
sessionStorageKey,
|
|
1706
|
+
team
|
|
1707
|
+
});
|
|
1708
|
+
const client = useMemo(() => createClient({
|
|
1709
|
+
apiToken: token ?? void 0,
|
|
1710
|
+
env,
|
|
1711
|
+
team
|
|
1712
|
+
}), [
|
|
1713
|
+
env,
|
|
1714
|
+
team,
|
|
1715
|
+
token
|
|
1716
|
+
]);
|
|
1717
|
+
const defaultDraftStore = useMemo(() => createContentDraftStore(`${sessionStorageKey}:draft`), [sessionStorageKey]);
|
|
1718
|
+
const [sessionExpired, setSessionExpired] = useState(false);
|
|
1719
|
+
const [loginUrl, setLoginUrl] = useState(null);
|
|
1720
|
+
useEffect(() => {
|
|
1721
|
+
if (!token) return;
|
|
1722
|
+
setSessionExpired(false);
|
|
1723
|
+
const previous = client.http.onUnauthorized;
|
|
1724
|
+
client.http.onUnauthorized = () => {
|
|
1725
|
+
setSessionExpired(true);
|
|
1726
|
+
};
|
|
1727
|
+
return () => {
|
|
1728
|
+
client.http.onUnauthorized = previous;
|
|
1729
|
+
};
|
|
1730
|
+
}, [token]);
|
|
1731
|
+
useEffect(() => {
|
|
1732
|
+
if (!sessionExpired || typeof window === "undefined") return;
|
|
1733
|
+
const returnTo = getContentSessionReturnUrl(window.location.href);
|
|
1734
|
+
setLoginUrl(typeof reauthenticateUrl === "function" ? reauthenticateUrl(returnTo) : reauthenticateUrl ?? getLoginUrl(env, returnTo, { team }));
|
|
1735
|
+
}, [
|
|
1736
|
+
env,
|
|
1737
|
+
sessionExpired,
|
|
1738
|
+
team
|
|
1739
|
+
]);
|
|
1740
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1741
|
+
error && /* @__PURE__ */ jsx("p", {
|
|
1742
|
+
className: authErrorClassName ?? "fixed right-4 bottom-4 z-[1001] max-w-[min(24rem,calc(100vw-2rem))] rounded-xl border border-red-200 bg-white p-3 text-sm text-red-700 shadow-xl",
|
|
1743
|
+
"data-content-auth-error": "true",
|
|
1744
|
+
role: "alert",
|
|
1745
|
+
children: authErrorMessage
|
|
1746
|
+
}),
|
|
1747
|
+
sessionExpired && /* @__PURE__ */ jsxs("div", {
|
|
1748
|
+
className: authErrorClassName ?? "fixed right-4 bottom-4 z-[1002] max-w-[min(26rem,calc(100vw-2rem))] rounded-xl border border-amber-200 bg-white p-4 text-sm text-slate-800 shadow-xl",
|
|
1749
|
+
"data-content-session-expired": "true",
|
|
1750
|
+
role: "alert",
|
|
1751
|
+
children: [/* @__PURE__ */ jsx("strong", {
|
|
1752
|
+
className: "block font-semibold text-slate-950",
|
|
1753
|
+
children: sessionExpiredMessage
|
|
1754
|
+
}), loginUrl && /* @__PURE__ */ jsx("a", {
|
|
1755
|
+
className: "mt-3 inline-flex rounded-lg bg-blue-600 px-3 py-2 font-semibold text-white transition hover:bg-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2",
|
|
1756
|
+
"data-content-navigation-allowed": true,
|
|
1757
|
+
href: loginUrl,
|
|
1758
|
+
children: reauthenticateLabel
|
|
1759
|
+
})]
|
|
1760
|
+
}),
|
|
1761
|
+
/* @__PURE__ */ jsx(ContentEditor, {
|
|
1762
|
+
client,
|
|
1763
|
+
contentId: token ? contentId : void 0,
|
|
1764
|
+
publicSlug: token ? void 0 : publicSlug,
|
|
1765
|
+
initialContent: token ? void 0 : initialContent,
|
|
1766
|
+
canEdit: Boolean(token),
|
|
1767
|
+
canPublish: Boolean(token),
|
|
1768
|
+
teamSlug: team,
|
|
1769
|
+
confirmLeave,
|
|
1770
|
+
confirmCancel,
|
|
1771
|
+
confirmPublish,
|
|
1772
|
+
draftStore: draftStore ?? defaultDraftStore,
|
|
1773
|
+
imagePickerLabels,
|
|
1774
|
+
labels,
|
|
1775
|
+
messages: {
|
|
1776
|
+
...messages,
|
|
1777
|
+
sessionExpired: sessionExpiredMessage
|
|
1778
|
+
},
|
|
1779
|
+
onUnauthorized: () => {
|
|
1780
|
+
setSessionExpired(true);
|
|
1781
|
+
},
|
|
1782
|
+
title,
|
|
1783
|
+
children
|
|
1784
|
+
})
|
|
1785
|
+
] });
|
|
1786
|
+
}
|
|
1787
|
+
//#endregion
|
|
1788
|
+
export { ContentEditor, ContentProvider, ContentToolbar, EditableContent, EditableImage, EmbedChatInput, EmbedChatList, ManagedContentEditor, clearRoom, containedImageDimensions, createContentDraftStore, createSessionTokenStore, cropOutputDimensions, getContentSessionReturnUrl, getOrCreateGuestId, imageCropRectangle, loadRoom, openEventSourceStream, processImageFile, readImageDimensions, saveRoom, useContentSession, useContentValue, useEmbedChat, useUnsavedNavigation, validateImageDimensions, validateImageFile };
|