@structcms/admin 0.1.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/LICENSE +21 -0
- package/README.md +180 -0
- package/dist/index.cjs +2761 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +736 -0
- package/dist/index.d.ts +736 -0
- package/dist/index.js +2686 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2686 @@
|
|
|
1
|
+
// src/components/app/struct-cms-admin-app.tsx
|
|
2
|
+
import { useEffect as useEffect8, useState as useState11 } from "react";
|
|
3
|
+
|
|
4
|
+
// src/context/admin-context.tsx
|
|
5
|
+
import { createContext as createContext2 } from "react";
|
|
6
|
+
|
|
7
|
+
// src/components/ui/toast.tsx
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
|
|
10
|
+
// src/lib/utils.ts
|
|
11
|
+
import { clsx } from "clsx";
|
|
12
|
+
import { twMerge } from "tailwind-merge";
|
|
13
|
+
function cn(...inputs) {
|
|
14
|
+
return twMerge(clsx(inputs));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/components/ui/toast.tsx
|
|
18
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
19
|
+
var ToastContext = React.createContext(null);
|
|
20
|
+
function ToastProvider({ children, autoDismissMs = 5e3 }) {
|
|
21
|
+
const [toasts, setToasts] = React.useState([]);
|
|
22
|
+
const counterRef = React.useRef(0);
|
|
23
|
+
const removeToast = React.useCallback((id) => {
|
|
24
|
+
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
25
|
+
}, []);
|
|
26
|
+
const addToast = React.useCallback(
|
|
27
|
+
(message, variant = "default") => {
|
|
28
|
+
const id = `toast-${++counterRef.current}`;
|
|
29
|
+
const toast = { id, message, variant };
|
|
30
|
+
setToasts((prev) => [...prev, toast]);
|
|
31
|
+
if (autoDismissMs > 0) {
|
|
32
|
+
setTimeout(() => removeToast(id), autoDismissMs);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
[autoDismissMs, removeToast]
|
|
36
|
+
);
|
|
37
|
+
const value = React.useMemo(
|
|
38
|
+
() => ({ toasts, addToast, removeToast }),
|
|
39
|
+
[toasts, addToast, removeToast]
|
|
40
|
+
);
|
|
41
|
+
return /* @__PURE__ */ jsxs(ToastContext.Provider, { value, children: [
|
|
42
|
+
children,
|
|
43
|
+
/* @__PURE__ */ jsx(ToastContainer, { toasts, onDismiss: removeToast })
|
|
44
|
+
] });
|
|
45
|
+
}
|
|
46
|
+
ToastProvider.displayName = "ToastProvider";
|
|
47
|
+
function useToast() {
|
|
48
|
+
const context = React.useContext(ToastContext);
|
|
49
|
+
if (!context) {
|
|
50
|
+
throw new Error("useToast must be used within a ToastProvider");
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
toast: context.addToast,
|
|
54
|
+
dismiss: context.removeToast,
|
|
55
|
+
toasts: context.toasts
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
var variantStyles = {
|
|
59
|
+
default: "bg-card border-input text-foreground",
|
|
60
|
+
success: "bg-card border-green-500 text-foreground",
|
|
61
|
+
error: "bg-card border-destructive text-foreground"
|
|
62
|
+
};
|
|
63
|
+
function ToastContainer({ toasts, onDismiss }) {
|
|
64
|
+
if (toasts.length === 0) return null;
|
|
65
|
+
return /* @__PURE__ */ jsx(
|
|
66
|
+
"div",
|
|
67
|
+
{
|
|
68
|
+
className: "fixed bottom-4 right-4 z-50 flex flex-col gap-2 max-w-sm",
|
|
69
|
+
"data-testid": "toast-container",
|
|
70
|
+
children: toasts.map((toast) => /* @__PURE__ */ jsxs(
|
|
71
|
+
"div",
|
|
72
|
+
{
|
|
73
|
+
className: cn(
|
|
74
|
+
"rounded-md border px-4 py-3 shadow-md flex items-center justify-between gap-2",
|
|
75
|
+
variantStyles[toast.variant ?? "default"]
|
|
76
|
+
),
|
|
77
|
+
role: "alert",
|
|
78
|
+
"data-testid": `toast-${toast.id}`,
|
|
79
|
+
children: [
|
|
80
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm", children: toast.message }),
|
|
81
|
+
/* @__PURE__ */ jsx(
|
|
82
|
+
"button",
|
|
83
|
+
{
|
|
84
|
+
type: "button",
|
|
85
|
+
className: "text-muted-foreground hover:text-foreground text-sm",
|
|
86
|
+
onClick: () => onDismiss(toast.id),
|
|
87
|
+
"data-testid": `toast-dismiss-${toast.id}`,
|
|
88
|
+
children: "\u2715"
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
toast.id
|
|
94
|
+
))
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/context/admin-context.tsx
|
|
100
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
101
|
+
var DEFAULT_API_BASE_URL = "/api/cms";
|
|
102
|
+
var AdminContext = createContext2(null);
|
|
103
|
+
function AdminProvider({
|
|
104
|
+
children,
|
|
105
|
+
registry,
|
|
106
|
+
apiBaseUrl = DEFAULT_API_BASE_URL
|
|
107
|
+
}) {
|
|
108
|
+
const value = {
|
|
109
|
+
registry,
|
|
110
|
+
apiBaseUrl
|
|
111
|
+
};
|
|
112
|
+
return /* @__PURE__ */ jsx2(AdminContext.Provider, { value, children: /* @__PURE__ */ jsx2(ToastProvider, { children }) });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/context/auth-context.tsx
|
|
116
|
+
import { createContext as createContext3, useCallback as useCallback2, useContext as useContext2, useEffect, useState as useState2 } from "react";
|
|
117
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
118
|
+
var AuthContext = createContext3(void 0);
|
|
119
|
+
function getCsrfToken() {
|
|
120
|
+
if (typeof document === "undefined") return null;
|
|
121
|
+
const match = document.cookie.match(/structcms_csrf_token=([^;]+)/);
|
|
122
|
+
return match?.[1] ?? null;
|
|
123
|
+
}
|
|
124
|
+
function createFetchOptions(method, body) {
|
|
125
|
+
const options = {
|
|
126
|
+
method,
|
|
127
|
+
credentials: "include",
|
|
128
|
+
// Always send cookies
|
|
129
|
+
headers: {
|
|
130
|
+
"Content-Type": "application/json"
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
134
|
+
const csrfToken = getCsrfToken();
|
|
135
|
+
if (csrfToken) {
|
|
136
|
+
options.headers["X-CSRF-Token"] = csrfToken;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (body) {
|
|
140
|
+
options.body = JSON.stringify(body);
|
|
141
|
+
}
|
|
142
|
+
return options;
|
|
143
|
+
}
|
|
144
|
+
function AuthProvider({ children, apiBaseUrl, onAuthStateChange }) {
|
|
145
|
+
const isAuthDisabled = process.env.NODE_ENV === "development" && typeof window !== "undefined" && (process.env.NEXT_PUBLIC_DISABLE_AUTH === "true" || // biome-ignore lint/suspicious/noExplicitAny: Next.js internal data structure
|
|
146
|
+
window.__NEXT_DATA__?.props?.pageProps?.disableAuth === true);
|
|
147
|
+
if (isAuthDisabled) {
|
|
148
|
+
console.warn("\u26A0\uFE0F WARNING: Authentication is DISABLED. This should only be used in development!");
|
|
149
|
+
}
|
|
150
|
+
const [user, setUser] = useState2(null);
|
|
151
|
+
const [session, setSession] = useState2(null);
|
|
152
|
+
const [isLoading, setIsLoading] = useState2(!isAuthDisabled);
|
|
153
|
+
const clearSession = useCallback2(() => {
|
|
154
|
+
setUser(null);
|
|
155
|
+
setSession(null);
|
|
156
|
+
onAuthStateChange?.(null);
|
|
157
|
+
}, [onAuthStateChange]);
|
|
158
|
+
const tryRefreshSession = useCallback2(async () => {
|
|
159
|
+
try {
|
|
160
|
+
const refreshResponse = await fetch(
|
|
161
|
+
`${apiBaseUrl}/auth/refresh`,
|
|
162
|
+
createFetchOptions("POST")
|
|
163
|
+
);
|
|
164
|
+
if (refreshResponse.ok) {
|
|
165
|
+
const data = await refreshResponse.json();
|
|
166
|
+
setSession({
|
|
167
|
+
accessToken: "",
|
|
168
|
+
// Not accessible from client
|
|
169
|
+
user: data.user,
|
|
170
|
+
expiresAt: data.expiresAt ? new Date(data.expiresAt) : void 0
|
|
171
|
+
});
|
|
172
|
+
setUser(data.user);
|
|
173
|
+
onAuthStateChange?.(data.user);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.error("Failed to refresh session:", error);
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}, [apiBaseUrl, onAuthStateChange]);
|
|
181
|
+
const loadSession = useCallback2(async () => {
|
|
182
|
+
if (isAuthDisabled) {
|
|
183
|
+
setIsLoading(false);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const response = await fetch(
|
|
188
|
+
`${apiBaseUrl}/auth/verify`,
|
|
189
|
+
createFetchOptions("POST")
|
|
190
|
+
);
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
const refreshed = await tryRefreshSession();
|
|
193
|
+
if (!refreshed) {
|
|
194
|
+
clearSession();
|
|
195
|
+
}
|
|
196
|
+
setIsLoading(false);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const userData = await response.json();
|
|
200
|
+
setUser(userData);
|
|
201
|
+
setSession({
|
|
202
|
+
accessToken: "",
|
|
203
|
+
// Not accessible from client
|
|
204
|
+
user: userData
|
|
205
|
+
});
|
|
206
|
+
onAuthStateChange?.(userData);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error("Failed to load session:", error);
|
|
209
|
+
clearSession();
|
|
210
|
+
} finally {
|
|
211
|
+
setIsLoading(false);
|
|
212
|
+
}
|
|
213
|
+
}, [apiBaseUrl, onAuthStateChange, isAuthDisabled, tryRefreshSession, clearSession]);
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
const fetchCsrfToken = async () => {
|
|
216
|
+
try {
|
|
217
|
+
await fetch(`${apiBaseUrl}/auth/csrf`, {
|
|
218
|
+
credentials: "include"
|
|
219
|
+
});
|
|
220
|
+
} catch (error) {
|
|
221
|
+
console.error("Failed to fetch CSRF token:", error);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
fetchCsrfToken();
|
|
225
|
+
}, [apiBaseUrl]);
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
loadSession();
|
|
228
|
+
}, [loadSession]);
|
|
229
|
+
const signIn = useCallback2(
|
|
230
|
+
async (email, password) => {
|
|
231
|
+
setIsLoading(true);
|
|
232
|
+
try {
|
|
233
|
+
const response = await fetch(
|
|
234
|
+
`${apiBaseUrl}/auth/signin`,
|
|
235
|
+
createFetchOptions("POST", { email, password })
|
|
236
|
+
);
|
|
237
|
+
if (!response.ok) {
|
|
238
|
+
const error = await response.json();
|
|
239
|
+
throw new Error(error.error?.message || error.message || "Sign in failed");
|
|
240
|
+
}
|
|
241
|
+
const data = await response.json();
|
|
242
|
+
setSession({
|
|
243
|
+
accessToken: "",
|
|
244
|
+
// Not accessible from client
|
|
245
|
+
user: data.user,
|
|
246
|
+
expiresAt: data.expiresAt ? new Date(data.expiresAt) : void 0
|
|
247
|
+
});
|
|
248
|
+
setUser(data.user);
|
|
249
|
+
onAuthStateChange?.(data.user);
|
|
250
|
+
} finally {
|
|
251
|
+
setIsLoading(false);
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
[apiBaseUrl, onAuthStateChange]
|
|
255
|
+
);
|
|
256
|
+
const signOut = useCallback2(async () => {
|
|
257
|
+
setIsLoading(true);
|
|
258
|
+
try {
|
|
259
|
+
await fetch(
|
|
260
|
+
`${apiBaseUrl}/auth/signout`,
|
|
261
|
+
createFetchOptions("POST")
|
|
262
|
+
);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error("Sign out error:", error);
|
|
265
|
+
} finally {
|
|
266
|
+
clearSession();
|
|
267
|
+
setIsLoading(false);
|
|
268
|
+
}
|
|
269
|
+
}, [apiBaseUrl, clearSession]);
|
|
270
|
+
const refreshSession = useCallback2(async () => {
|
|
271
|
+
const response = await fetch(
|
|
272
|
+
`${apiBaseUrl}/auth/refresh`,
|
|
273
|
+
createFetchOptions("POST")
|
|
274
|
+
);
|
|
275
|
+
if (!response.ok) {
|
|
276
|
+
throw new Error("Failed to refresh session");
|
|
277
|
+
}
|
|
278
|
+
const data = await response.json();
|
|
279
|
+
setSession({
|
|
280
|
+
accessToken: "",
|
|
281
|
+
// Not accessible from client
|
|
282
|
+
user: data.user,
|
|
283
|
+
expiresAt: data.expiresAt ? new Date(data.expiresAt) : void 0
|
|
284
|
+
});
|
|
285
|
+
setUser(data.user);
|
|
286
|
+
onAuthStateChange?.(data.user);
|
|
287
|
+
}, [apiBaseUrl, onAuthStateChange]);
|
|
288
|
+
const value = {
|
|
289
|
+
user,
|
|
290
|
+
session,
|
|
291
|
+
isLoading,
|
|
292
|
+
isAuthenticated: isAuthDisabled || !!user,
|
|
293
|
+
signIn,
|
|
294
|
+
signOut,
|
|
295
|
+
refreshSession
|
|
296
|
+
};
|
|
297
|
+
return /* @__PURE__ */ jsx3(AuthContext.Provider, { value, children });
|
|
298
|
+
}
|
|
299
|
+
function useAuth() {
|
|
300
|
+
const context = useContext2(AuthContext);
|
|
301
|
+
if (!context) {
|
|
302
|
+
throw new Error("useAuth must be used within an AuthProvider");
|
|
303
|
+
}
|
|
304
|
+
return context;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/hooks/use-api-client.ts
|
|
308
|
+
import { useMemo as useMemo2 } from "react";
|
|
309
|
+
|
|
310
|
+
// src/hooks/use-admin.ts
|
|
311
|
+
import { useContext as useContext3 } from "react";
|
|
312
|
+
function useAdmin() {
|
|
313
|
+
const context = useContext3(AdminContext);
|
|
314
|
+
if (context === null) {
|
|
315
|
+
throw new Error("useAdmin must be used within an AdminProvider");
|
|
316
|
+
}
|
|
317
|
+
return context;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/hooks/use-api-client.ts
|
|
321
|
+
async function handleResponse(response) {
|
|
322
|
+
if (!response.ok) {
|
|
323
|
+
let errorMessage = "An error occurred";
|
|
324
|
+
let errorCode;
|
|
325
|
+
try {
|
|
326
|
+
const errorBody = await response.json();
|
|
327
|
+
if (errorBody.error?.message) {
|
|
328
|
+
errorMessage = errorBody.error.message;
|
|
329
|
+
}
|
|
330
|
+
if (errorBody.error?.code) {
|
|
331
|
+
errorCode = errorBody.error.code;
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
errorMessage = response.statusText || errorMessage;
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
data: null,
|
|
338
|
+
error: {
|
|
339
|
+
message: errorMessage,
|
|
340
|
+
code: errorCode,
|
|
341
|
+
status: response.status
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
const data = await response.json();
|
|
347
|
+
return { data, error: null };
|
|
348
|
+
} catch {
|
|
349
|
+
return { data: null, error: null };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function createApiClient(baseUrl) {
|
|
353
|
+
const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
354
|
+
return {
|
|
355
|
+
async get(path) {
|
|
356
|
+
const response = await fetch(`${normalizedBaseUrl}${path}`, {
|
|
357
|
+
method: "GET",
|
|
358
|
+
headers: {
|
|
359
|
+
"Content-Type": "application/json"
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
return handleResponse(response);
|
|
363
|
+
},
|
|
364
|
+
async post(path, body) {
|
|
365
|
+
const response = await fetch(`${normalizedBaseUrl}${path}`, {
|
|
366
|
+
method: "POST",
|
|
367
|
+
headers: {
|
|
368
|
+
"Content-Type": "application/json"
|
|
369
|
+
},
|
|
370
|
+
body: JSON.stringify(body)
|
|
371
|
+
});
|
|
372
|
+
return handleResponse(response);
|
|
373
|
+
},
|
|
374
|
+
async put(path, body) {
|
|
375
|
+
const response = await fetch(`${normalizedBaseUrl}${path}`, {
|
|
376
|
+
method: "PUT",
|
|
377
|
+
headers: {
|
|
378
|
+
"Content-Type": "application/json"
|
|
379
|
+
},
|
|
380
|
+
body: JSON.stringify(body)
|
|
381
|
+
});
|
|
382
|
+
return handleResponse(response);
|
|
383
|
+
},
|
|
384
|
+
async delete(path) {
|
|
385
|
+
const response = await fetch(`${normalizedBaseUrl}${path}`, {
|
|
386
|
+
method: "DELETE",
|
|
387
|
+
headers: {
|
|
388
|
+
"Content-Type": "application/json"
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
return handleResponse(response);
|
|
392
|
+
},
|
|
393
|
+
async upload(path, body) {
|
|
394
|
+
const response = await fetch(`${normalizedBaseUrl}${path}`, {
|
|
395
|
+
method: "POST",
|
|
396
|
+
body
|
|
397
|
+
});
|
|
398
|
+
return handleResponse(response);
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function useApiClient() {
|
|
403
|
+
const { apiBaseUrl } = useAdmin();
|
|
404
|
+
return useMemo2(() => createApiClient(apiBaseUrl), [apiBaseUrl]);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/components/content/navigation-editor.tsx
|
|
408
|
+
import * as React4 from "react";
|
|
409
|
+
|
|
410
|
+
// src/components/ui/button.tsx
|
|
411
|
+
import { cva } from "class-variance-authority";
|
|
412
|
+
import * as React2 from "react";
|
|
413
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
414
|
+
var buttonVariants = cva(
|
|
415
|
+
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
416
|
+
{
|
|
417
|
+
variants: {
|
|
418
|
+
variant: {
|
|
419
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
420
|
+
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
421
|
+
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
422
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
423
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
424
|
+
link: "text-primary underline-offset-4 hover:underline"
|
|
425
|
+
},
|
|
426
|
+
size: {
|
|
427
|
+
default: "h-10 px-4 py-2",
|
|
428
|
+
sm: "h-9 rounded-md px-3",
|
|
429
|
+
lg: "h-11 rounded-md px-8",
|
|
430
|
+
icon: "h-10 w-10"
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
defaultVariants: {
|
|
434
|
+
variant: "default",
|
|
435
|
+
size: "default"
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
);
|
|
439
|
+
var Button = React2.forwardRef(
|
|
440
|
+
({ className, variant, size, ...props }, ref) => {
|
|
441
|
+
return /* @__PURE__ */ jsx4("button", { className: cn(buttonVariants({ variant, size, className })), ref, ...props });
|
|
442
|
+
}
|
|
443
|
+
);
|
|
444
|
+
Button.displayName = "Button";
|
|
445
|
+
|
|
446
|
+
// src/components/ui/input.tsx
|
|
447
|
+
import * as React3 from "react";
|
|
448
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
449
|
+
var Input = React3.forwardRef(
|
|
450
|
+
({ className, type, ...props }, ref) => {
|
|
451
|
+
return /* @__PURE__ */ jsx5(
|
|
452
|
+
"input",
|
|
453
|
+
{
|
|
454
|
+
type,
|
|
455
|
+
className: cn(
|
|
456
|
+
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
457
|
+
className
|
|
458
|
+
),
|
|
459
|
+
ref,
|
|
460
|
+
...props
|
|
461
|
+
}
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
);
|
|
465
|
+
Input.displayName = "Input";
|
|
466
|
+
|
|
467
|
+
// src/components/content/navigation-editor.tsx
|
|
468
|
+
import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
469
|
+
function NavigationEditor({ items: initialItems, onSave, className }) {
|
|
470
|
+
const [items, setItems] = React4.useState(initialItems);
|
|
471
|
+
const handleAddItem = () => {
|
|
472
|
+
setItems([...items, { label: "", href: "", children: [] }]);
|
|
473
|
+
};
|
|
474
|
+
const handleRemoveItem = (index) => {
|
|
475
|
+
const newItems = [...items];
|
|
476
|
+
newItems.splice(index, 1);
|
|
477
|
+
setItems(newItems);
|
|
478
|
+
};
|
|
479
|
+
const handleItemChange = (index, field, value) => {
|
|
480
|
+
const newItems = [...items];
|
|
481
|
+
const item = newItems[index];
|
|
482
|
+
if (item) {
|
|
483
|
+
newItems[index] = { ...item, [field]: value };
|
|
484
|
+
}
|
|
485
|
+
setItems(newItems);
|
|
486
|
+
};
|
|
487
|
+
const handleAddChild = (parentIndex) => {
|
|
488
|
+
const newItems = [...items];
|
|
489
|
+
const parent = newItems[parentIndex];
|
|
490
|
+
if (parent) {
|
|
491
|
+
newItems[parentIndex] = {
|
|
492
|
+
...parent,
|
|
493
|
+
children: [...parent.children ?? [], { label: "", href: "" }]
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
setItems(newItems);
|
|
497
|
+
};
|
|
498
|
+
const handleRemoveChild = (parentIndex, childIndex) => {
|
|
499
|
+
const newItems = [...items];
|
|
500
|
+
const parent = newItems[parentIndex];
|
|
501
|
+
if (parent?.children) {
|
|
502
|
+
const newChildren = [...parent.children];
|
|
503
|
+
newChildren.splice(childIndex, 1);
|
|
504
|
+
newItems[parentIndex] = { ...parent, children: newChildren };
|
|
505
|
+
}
|
|
506
|
+
setItems(newItems);
|
|
507
|
+
};
|
|
508
|
+
const handleChildChange = (parentIndex, childIndex, field, value) => {
|
|
509
|
+
const newItems = [...items];
|
|
510
|
+
const parent = newItems[parentIndex];
|
|
511
|
+
if (parent?.children) {
|
|
512
|
+
const newChildren = [...parent.children];
|
|
513
|
+
const child = newChildren[childIndex];
|
|
514
|
+
if (child) {
|
|
515
|
+
newChildren[childIndex] = { ...child, [field]: value };
|
|
516
|
+
newItems[parentIndex] = { ...parent, children: newChildren };
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
setItems(newItems);
|
|
520
|
+
};
|
|
521
|
+
const handleSave = () => {
|
|
522
|
+
onSave(items);
|
|
523
|
+
};
|
|
524
|
+
return /* @__PURE__ */ jsxs2("div", { className: cn("space-y-4", className), "data-testid": "navigation-editor", children: [
|
|
525
|
+
/* @__PURE__ */ jsx6("div", { className: "flex items-center justify-between", children: /* @__PURE__ */ jsx6("h2", { className: "text-xl font-semibold", children: "Navigation" }) }),
|
|
526
|
+
items.length === 0 ? /* @__PURE__ */ jsx6("p", { className: "text-sm text-muted-foreground text-center py-8", "data-testid": "empty-state", children: "No navigation items yet." }) : /* @__PURE__ */ jsx6("div", { className: "space-y-3", children: items.map((item, index) => /* @__PURE__ */ jsxs2(
|
|
527
|
+
"div",
|
|
528
|
+
{
|
|
529
|
+
className: "rounded-md border border-input bg-background p-4 space-y-3",
|
|
530
|
+
"data-testid": `nav-item-${index}`,
|
|
531
|
+
children: [
|
|
532
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex gap-2 items-start", children: [
|
|
533
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex-1 space-y-2", children: [
|
|
534
|
+
/* @__PURE__ */ jsx6(
|
|
535
|
+
Input,
|
|
536
|
+
{
|
|
537
|
+
placeholder: "Label",
|
|
538
|
+
value: item.label,
|
|
539
|
+
onChange: (e) => handleItemChange(index, "label", e.target.value),
|
|
540
|
+
"data-testid": `nav-item-label-${index}`
|
|
541
|
+
}
|
|
542
|
+
),
|
|
543
|
+
/* @__PURE__ */ jsx6(
|
|
544
|
+
Input,
|
|
545
|
+
{
|
|
546
|
+
placeholder: "URL (e.g. /about)",
|
|
547
|
+
value: item.href,
|
|
548
|
+
onChange: (e) => handleItemChange(index, "href", e.target.value),
|
|
549
|
+
"data-testid": `nav-item-href-${index}`
|
|
550
|
+
}
|
|
551
|
+
)
|
|
552
|
+
] }),
|
|
553
|
+
/* @__PURE__ */ jsx6(
|
|
554
|
+
Button,
|
|
555
|
+
{
|
|
556
|
+
type: "button",
|
|
557
|
+
variant: "ghost",
|
|
558
|
+
size: "icon",
|
|
559
|
+
onClick: () => handleRemoveItem(index),
|
|
560
|
+
title: "Remove item",
|
|
561
|
+
"data-testid": `nav-item-remove-${index}`,
|
|
562
|
+
children: "\u2715"
|
|
563
|
+
}
|
|
564
|
+
)
|
|
565
|
+
] }),
|
|
566
|
+
(item.children ?? []).length > 0 && /* @__PURE__ */ jsx6("div", { className: "ml-6 space-y-2", children: (item.children ?? []).map((child, childIndex) => /* @__PURE__ */ jsxs2(
|
|
567
|
+
"div",
|
|
568
|
+
{
|
|
569
|
+
className: "flex gap-2 items-start rounded-md border border-input bg-muted/30 p-3",
|
|
570
|
+
"data-testid": `nav-child-${index}-${childIndex}`,
|
|
571
|
+
children: [
|
|
572
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex-1 space-y-2", children: [
|
|
573
|
+
/* @__PURE__ */ jsx6(
|
|
574
|
+
Input,
|
|
575
|
+
{
|
|
576
|
+
placeholder: "Label",
|
|
577
|
+
value: child.label,
|
|
578
|
+
onChange: (e) => handleChildChange(index, childIndex, "label", e.target.value),
|
|
579
|
+
"data-testid": `nav-child-label-${index}-${childIndex}`
|
|
580
|
+
}
|
|
581
|
+
),
|
|
582
|
+
/* @__PURE__ */ jsx6(
|
|
583
|
+
Input,
|
|
584
|
+
{
|
|
585
|
+
placeholder: "URL",
|
|
586
|
+
value: child.href,
|
|
587
|
+
onChange: (e) => handleChildChange(index, childIndex, "href", e.target.value),
|
|
588
|
+
"data-testid": `nav-child-href-${index}-${childIndex}`
|
|
589
|
+
}
|
|
590
|
+
)
|
|
591
|
+
] }),
|
|
592
|
+
/* @__PURE__ */ jsx6(
|
|
593
|
+
Button,
|
|
594
|
+
{
|
|
595
|
+
type: "button",
|
|
596
|
+
variant: "ghost",
|
|
597
|
+
size: "icon",
|
|
598
|
+
onClick: () => handleRemoveChild(index, childIndex),
|
|
599
|
+
title: "Remove child",
|
|
600
|
+
"data-testid": `nav-child-remove-${index}-${childIndex}`,
|
|
601
|
+
children: "\u2715"
|
|
602
|
+
}
|
|
603
|
+
)
|
|
604
|
+
]
|
|
605
|
+
},
|
|
606
|
+
childIndex
|
|
607
|
+
)) }),
|
|
608
|
+
/* @__PURE__ */ jsx6(
|
|
609
|
+
Button,
|
|
610
|
+
{
|
|
611
|
+
type: "button",
|
|
612
|
+
variant: "outline",
|
|
613
|
+
size: "sm",
|
|
614
|
+
onClick: () => handleAddChild(index),
|
|
615
|
+
"data-testid": `nav-add-child-${index}`,
|
|
616
|
+
children: "Add Child"
|
|
617
|
+
}
|
|
618
|
+
)
|
|
619
|
+
]
|
|
620
|
+
},
|
|
621
|
+
index
|
|
622
|
+
)) }),
|
|
623
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex gap-2 border-t border-input pt-4", children: [
|
|
624
|
+
/* @__PURE__ */ jsx6(Button, { type: "button", variant: "outline", onClick: handleAddItem, "data-testid": "nav-add-item", children: "Add Item" }),
|
|
625
|
+
/* @__PURE__ */ jsx6(Button, { type: "button", onClick: handleSave, "data-testid": "nav-save", children: "Save Navigation" })
|
|
626
|
+
] })
|
|
627
|
+
] });
|
|
628
|
+
}
|
|
629
|
+
NavigationEditor.displayName = "NavigationEditor";
|
|
630
|
+
|
|
631
|
+
// src/components/content/page-list.tsx
|
|
632
|
+
import * as React5 from "react";
|
|
633
|
+
import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
634
|
+
function PageList({ onSelectPage, onCreatePage, className }) {
|
|
635
|
+
const api = useApiClient();
|
|
636
|
+
const { registry } = useAdmin();
|
|
637
|
+
const [pages, setPages] = React5.useState([]);
|
|
638
|
+
const [loading, setLoading] = React5.useState(true);
|
|
639
|
+
const [error, setError] = React5.useState(null);
|
|
640
|
+
const [search, setSearch] = React5.useState("");
|
|
641
|
+
const [pageTypeFilter, setPageTypeFilter] = React5.useState("");
|
|
642
|
+
const pageTypes = registry.getAllPageTypes();
|
|
643
|
+
React5.useEffect(() => {
|
|
644
|
+
let cancelled = false;
|
|
645
|
+
async function fetchPages() {
|
|
646
|
+
setLoading(true);
|
|
647
|
+
setError(null);
|
|
648
|
+
const result = await api.get("/pages");
|
|
649
|
+
if (cancelled) return;
|
|
650
|
+
if (result.error) {
|
|
651
|
+
setError(result.error.message);
|
|
652
|
+
setLoading(false);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
setPages(result.data ?? []);
|
|
656
|
+
setLoading(false);
|
|
657
|
+
}
|
|
658
|
+
void fetchPages();
|
|
659
|
+
return () => {
|
|
660
|
+
cancelled = true;
|
|
661
|
+
};
|
|
662
|
+
}, [api]);
|
|
663
|
+
const filteredPages = React5.useMemo(() => {
|
|
664
|
+
let result = pages;
|
|
665
|
+
if (search) {
|
|
666
|
+
const lowerSearch = search.toLowerCase();
|
|
667
|
+
result = result.filter(
|
|
668
|
+
(page) => page.title.toLowerCase().includes(lowerSearch) || page.slug.toLowerCase().includes(lowerSearch)
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
if (pageTypeFilter) {
|
|
672
|
+
result = result.filter((page) => page.pageType === pageTypeFilter);
|
|
673
|
+
}
|
|
674
|
+
return result;
|
|
675
|
+
}, [pages, search, pageTypeFilter]);
|
|
676
|
+
return /* @__PURE__ */ jsxs3("div", { className: cn("space-y-4", className), "data-testid": "page-list", children: [
|
|
677
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between", children: [
|
|
678
|
+
/* @__PURE__ */ jsx7("h2", { className: "text-xl font-semibold", children: "Pages" }),
|
|
679
|
+
/* @__PURE__ */ jsx7(Button, { type: "button", onClick: onCreatePage, "data-testid": "create-page", children: "Create New Page" })
|
|
680
|
+
] }),
|
|
681
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex gap-2", children: [
|
|
682
|
+
/* @__PURE__ */ jsx7(
|
|
683
|
+
Input,
|
|
684
|
+
{
|
|
685
|
+
placeholder: "Search by title or slug...",
|
|
686
|
+
value: search,
|
|
687
|
+
onChange: (e) => setSearch(e.target.value),
|
|
688
|
+
className: "max-w-sm",
|
|
689
|
+
"data-testid": "search-input"
|
|
690
|
+
}
|
|
691
|
+
),
|
|
692
|
+
pageTypes.length > 0 && /* @__PURE__ */ jsxs3(
|
|
693
|
+
"select",
|
|
694
|
+
{
|
|
695
|
+
value: pageTypeFilter,
|
|
696
|
+
onChange: (e) => setPageTypeFilter(e.target.value),
|
|
697
|
+
className: "flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm",
|
|
698
|
+
"data-testid": "page-type-filter",
|
|
699
|
+
children: [
|
|
700
|
+
/* @__PURE__ */ jsx7("option", { value: "", children: "All Types" }),
|
|
701
|
+
pageTypes.map((pt) => /* @__PURE__ */ jsx7("option", { value: pt.name, children: pt.name }, pt.name))
|
|
702
|
+
]
|
|
703
|
+
}
|
|
704
|
+
)
|
|
705
|
+
] }),
|
|
706
|
+
loading && /* @__PURE__ */ jsx7("p", { className: "text-sm text-muted-foreground", "data-testid": "loading", children: "Loading pages..." }),
|
|
707
|
+
error && /* @__PURE__ */ jsx7("p", { className: "text-sm text-destructive", "data-testid": "error", children: error }),
|
|
708
|
+
!loading && !error && filteredPages.length === 0 && /* @__PURE__ */ jsx7("p", { className: "text-sm text-muted-foreground text-center py-8", "data-testid": "empty-state", children: pages.length === 0 ? "No pages yet. Create your first page." : "No pages match your search." }),
|
|
709
|
+
!loading && !error && filteredPages.length > 0 && /* @__PURE__ */ jsx7("div", { className: "rounded-md border border-input", "data-testid": "page-table", children: /* @__PURE__ */ jsxs3("table", { className: "w-full text-sm", children: [
|
|
710
|
+
/* @__PURE__ */ jsx7("thead", { children: /* @__PURE__ */ jsxs3("tr", { className: "border-b border-input bg-muted/50", children: [
|
|
711
|
+
/* @__PURE__ */ jsx7("th", { className: "text-left p-3 font-medium", children: "Title" }),
|
|
712
|
+
/* @__PURE__ */ jsx7("th", { className: "text-left p-3 font-medium", children: "Slug" }),
|
|
713
|
+
/* @__PURE__ */ jsx7("th", { className: "text-left p-3 font-medium", children: "Type" })
|
|
714
|
+
] }) }),
|
|
715
|
+
/* @__PURE__ */ jsx7("tbody", { children: filteredPages.map((page) => /* @__PURE__ */ jsxs3(
|
|
716
|
+
"tr",
|
|
717
|
+
{
|
|
718
|
+
className: "border-b border-input last:border-0 hover:bg-muted/30 cursor-pointer",
|
|
719
|
+
onClick: () => onSelectPage(page),
|
|
720
|
+
onKeyDown: (e) => {
|
|
721
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
722
|
+
e.preventDefault();
|
|
723
|
+
onSelectPage(page);
|
|
724
|
+
}
|
|
725
|
+
},
|
|
726
|
+
tabIndex: 0,
|
|
727
|
+
"data-testid": `page-row-${page.id}`,
|
|
728
|
+
children: [
|
|
729
|
+
/* @__PURE__ */ jsx7("td", { className: "p-3", children: page.title }),
|
|
730
|
+
/* @__PURE__ */ jsx7("td", { className: "p-3 text-muted-foreground", children: page.slug }),
|
|
731
|
+
/* @__PURE__ */ jsx7("td", { className: "p-3 text-muted-foreground capitalize", children: page.pageType })
|
|
732
|
+
]
|
|
733
|
+
},
|
|
734
|
+
page.id
|
|
735
|
+
)) })
|
|
736
|
+
] }) })
|
|
737
|
+
] });
|
|
738
|
+
}
|
|
739
|
+
PageList.displayName = "PageList";
|
|
740
|
+
|
|
741
|
+
// src/components/ui/error-boundary.tsx
|
|
742
|
+
import * as React6 from "react";
|
|
743
|
+
import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
744
|
+
var ErrorBoundary = class extends React6.Component {
|
|
745
|
+
constructor(props) {
|
|
746
|
+
super(props);
|
|
747
|
+
this.state = { hasError: false, error: null };
|
|
748
|
+
}
|
|
749
|
+
static getDerivedStateFromError(error) {
|
|
750
|
+
return { hasError: true, error };
|
|
751
|
+
}
|
|
752
|
+
handleReset = () => {
|
|
753
|
+
this.setState({ hasError: false, error: null });
|
|
754
|
+
this.props.onReset?.();
|
|
755
|
+
};
|
|
756
|
+
render() {
|
|
757
|
+
if (this.state.hasError) {
|
|
758
|
+
if (this.props.fallback) {
|
|
759
|
+
return this.props.fallback;
|
|
760
|
+
}
|
|
761
|
+
return /* @__PURE__ */ jsxs4(
|
|
762
|
+
"div",
|
|
763
|
+
{
|
|
764
|
+
className: cn(
|
|
765
|
+
"rounded-md border border-destructive bg-destructive/10 p-4",
|
|
766
|
+
this.props.className
|
|
767
|
+
),
|
|
768
|
+
role: "alert",
|
|
769
|
+
"data-testid": "error-boundary",
|
|
770
|
+
children: [
|
|
771
|
+
/* @__PURE__ */ jsx8("h3", { className: "text-sm font-semibold text-destructive mb-1", children: "Something went wrong" }),
|
|
772
|
+
/* @__PURE__ */ jsx8("p", { className: "text-sm text-muted-foreground", children: this.state.error?.message ?? "An unexpected error occurred" }),
|
|
773
|
+
/* @__PURE__ */ jsx8(
|
|
774
|
+
Button,
|
|
775
|
+
{
|
|
776
|
+
type: "button",
|
|
777
|
+
variant: "outline",
|
|
778
|
+
size: "sm",
|
|
779
|
+
className: "mt-3",
|
|
780
|
+
onClick: this.handleReset,
|
|
781
|
+
"data-testid": "error-boundary-retry",
|
|
782
|
+
children: "Retry"
|
|
783
|
+
}
|
|
784
|
+
)
|
|
785
|
+
]
|
|
786
|
+
}
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
return this.props.children;
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
// src/components/dashboard/kpi-cards.tsx
|
|
794
|
+
import * as React7 from "react";
|
|
795
|
+
|
|
796
|
+
// src/components/ui/skeleton.tsx
|
|
797
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
798
|
+
function Skeleton({ className, width, height, style, ...props }) {
|
|
799
|
+
return /* @__PURE__ */ jsx9(
|
|
800
|
+
"div",
|
|
801
|
+
{
|
|
802
|
+
className: cn("animate-pulse rounded-md bg-muted", className),
|
|
803
|
+
style: { width, height, ...style },
|
|
804
|
+
"data-testid": "skeleton",
|
|
805
|
+
...props
|
|
806
|
+
}
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
Skeleton.displayName = "Skeleton";
|
|
810
|
+
|
|
811
|
+
// src/components/dashboard/kpi-cards.tsx
|
|
812
|
+
import { jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
813
|
+
function KpiCards({ className }) {
|
|
814
|
+
const api = useApiClient();
|
|
815
|
+
const { registry } = useAdmin();
|
|
816
|
+
const [pages, setPages] = React7.useState({ value: null, loading: true, error: null });
|
|
817
|
+
const [media, setMedia] = React7.useState({ value: null, loading: true, error: null });
|
|
818
|
+
const [navigation, setNavigation] = React7.useState({
|
|
819
|
+
value: null,
|
|
820
|
+
loading: true,
|
|
821
|
+
error: null
|
|
822
|
+
});
|
|
823
|
+
const sectionsCount = registry.getAllSections().length;
|
|
824
|
+
const fetchPages = React7.useCallback(
|
|
825
|
+
async (signal) => {
|
|
826
|
+
setPages({ value: null, loading: true, error: null });
|
|
827
|
+
const result = await api.get("/pages");
|
|
828
|
+
if (signal?.cancelled) return;
|
|
829
|
+
if (result.error) {
|
|
830
|
+
setPages({ value: null, loading: false, error: result.error.message });
|
|
831
|
+
} else {
|
|
832
|
+
setPages({ value: result.data?.length ?? 0, loading: false, error: null });
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
[api]
|
|
836
|
+
);
|
|
837
|
+
const fetchMedia = React7.useCallback(
|
|
838
|
+
async (signal) => {
|
|
839
|
+
setMedia({ value: null, loading: true, error: null });
|
|
840
|
+
const result = await api.get("/media");
|
|
841
|
+
if (signal?.cancelled) return;
|
|
842
|
+
if (result.error) {
|
|
843
|
+
setMedia({ value: null, loading: false, error: result.error.message });
|
|
844
|
+
} else {
|
|
845
|
+
setMedia({ value: result.data?.length ?? 0, loading: false, error: null });
|
|
846
|
+
}
|
|
847
|
+
},
|
|
848
|
+
[api]
|
|
849
|
+
);
|
|
850
|
+
const fetchNavigation = React7.useCallback(
|
|
851
|
+
async (signal) => {
|
|
852
|
+
setNavigation({ value: null, loading: true, error: null });
|
|
853
|
+
const result = await api.get("/navigation");
|
|
854
|
+
if (signal?.cancelled) return;
|
|
855
|
+
if (result.error) {
|
|
856
|
+
setNavigation({ value: null, loading: false, error: result.error.message });
|
|
857
|
+
} else {
|
|
858
|
+
setNavigation({ value: result.data?.length ?? 0, loading: false, error: null });
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
[api]
|
|
862
|
+
);
|
|
863
|
+
React7.useEffect(() => {
|
|
864
|
+
const signal = { cancelled: false };
|
|
865
|
+
void Promise.allSettled([fetchPages(signal), fetchMedia(signal), fetchNavigation(signal)]);
|
|
866
|
+
return () => {
|
|
867
|
+
signal.cancelled = true;
|
|
868
|
+
};
|
|
869
|
+
}, [fetchPages, fetchMedia, fetchNavigation]);
|
|
870
|
+
const kpis = [
|
|
871
|
+
{ label: "Pages", state: pages, onRetry: () => void fetchPages(), testId: "kpi-pages" },
|
|
872
|
+
{ label: "Media Files", state: media, onRetry: () => void fetchMedia(), testId: "kpi-media" },
|
|
873
|
+
{
|
|
874
|
+
label: "Navigation Sets",
|
|
875
|
+
state: navigation,
|
|
876
|
+
onRetry: () => void fetchNavigation(),
|
|
877
|
+
testId: "kpi-navigation"
|
|
878
|
+
},
|
|
879
|
+
{
|
|
880
|
+
label: "Sections",
|
|
881
|
+
state: { value: sectionsCount, loading: false, error: null },
|
|
882
|
+
onRetry: void 0,
|
|
883
|
+
testId: "kpi-sections"
|
|
884
|
+
}
|
|
885
|
+
];
|
|
886
|
+
return /* @__PURE__ */ jsx10("div", { className: cn("grid grid-cols-2 gap-4 md:grid-cols-4", className), "data-testid": "kpi-cards", children: kpis.map((kpi) => /* @__PURE__ */ jsxs5(
|
|
887
|
+
"div",
|
|
888
|
+
{
|
|
889
|
+
className: "rounded-lg border border-input bg-background p-4",
|
|
890
|
+
"data-testid": kpi.testId,
|
|
891
|
+
children: [
|
|
892
|
+
/* @__PURE__ */ jsx10("p", { className: "text-sm text-muted-foreground", children: kpi.label }),
|
|
893
|
+
kpi.state.loading && /* @__PURE__ */ jsx10(Skeleton, { className: "mt-2 h-8 w-16", "data-testid": `${kpi.testId}-skeleton` }),
|
|
894
|
+
!kpi.state.loading && kpi.state.error && /* @__PURE__ */ jsxs5("div", { className: "mt-1", children: [
|
|
895
|
+
/* @__PURE__ */ jsx10("p", { className: "text-sm text-destructive", "data-testid": `${kpi.testId}-error`, children: "Error loading" }),
|
|
896
|
+
kpi.onRetry && /* @__PURE__ */ jsx10(
|
|
897
|
+
Button,
|
|
898
|
+
{
|
|
899
|
+
type: "button",
|
|
900
|
+
variant: "outline",
|
|
901
|
+
size: "sm",
|
|
902
|
+
className: "mt-2",
|
|
903
|
+
onClick: kpi.onRetry,
|
|
904
|
+
"data-testid": `${kpi.testId}-retry`,
|
|
905
|
+
children: "Retry"
|
|
906
|
+
}
|
|
907
|
+
)
|
|
908
|
+
] }),
|
|
909
|
+
!kpi.state.loading && !kpi.state.error && kpi.state.value !== null && /* @__PURE__ */ jsx10("p", { className: "mt-1 text-2xl font-bold", "data-testid": `${kpi.testId}-value`, children: kpi.state.value })
|
|
910
|
+
]
|
|
911
|
+
},
|
|
912
|
+
kpi.testId
|
|
913
|
+
)) });
|
|
914
|
+
}
|
|
915
|
+
KpiCards.displayName = "KpiCards";
|
|
916
|
+
|
|
917
|
+
// src/components/dashboard/quick-actions.tsx
|
|
918
|
+
import { jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
919
|
+
function QuickActions({ onCreatePage, onUploadMedia, className }) {
|
|
920
|
+
return /* @__PURE__ */ jsxs6("div", { className: cn("space-y-3", className), "data-testid": "quick-actions", children: [
|
|
921
|
+
/* @__PURE__ */ jsx11("h2", { className: "text-lg font-semibold", children: "Quick Actions" }),
|
|
922
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex flex-wrap gap-3", children: [
|
|
923
|
+
/* @__PURE__ */ jsx11(
|
|
924
|
+
Button,
|
|
925
|
+
{
|
|
926
|
+
onClick: onCreatePage,
|
|
927
|
+
"aria-label": "Create New Page",
|
|
928
|
+
"data-testid": "quick-action-create-page",
|
|
929
|
+
children: "Create New Page"
|
|
930
|
+
}
|
|
931
|
+
),
|
|
932
|
+
/* @__PURE__ */ jsx11(
|
|
933
|
+
Button,
|
|
934
|
+
{
|
|
935
|
+
variant: "outline",
|
|
936
|
+
onClick: onUploadMedia,
|
|
937
|
+
"aria-label": "Upload Media",
|
|
938
|
+
"data-testid": "quick-action-upload-media",
|
|
939
|
+
children: "Upload Media"
|
|
940
|
+
}
|
|
941
|
+
)
|
|
942
|
+
] })
|
|
943
|
+
] });
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// src/components/dashboard/recent-pages.tsx
|
|
947
|
+
import * as React8 from "react";
|
|
948
|
+
import { jsx as jsx12, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
949
|
+
function formatTimestamp(dateString) {
|
|
950
|
+
try {
|
|
951
|
+
return new Date(dateString).toLocaleDateString(void 0, {
|
|
952
|
+
year: "numeric",
|
|
953
|
+
month: "short",
|
|
954
|
+
day: "numeric"
|
|
955
|
+
});
|
|
956
|
+
} catch {
|
|
957
|
+
return dateString;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function RecentPages({ onSelectPage, className }) {
|
|
961
|
+
const api = useApiClient();
|
|
962
|
+
const [pages, setPages] = React8.useState([]);
|
|
963
|
+
const [loading, setLoading] = React8.useState(true);
|
|
964
|
+
const [error, setError] = React8.useState(false);
|
|
965
|
+
const fetchPages = React8.useCallback(
|
|
966
|
+
async (signal) => {
|
|
967
|
+
setLoading(true);
|
|
968
|
+
setError(false);
|
|
969
|
+
const result = await api.get("/pages");
|
|
970
|
+
if (signal?.cancelled) return;
|
|
971
|
+
if (result.error) {
|
|
972
|
+
setError(true);
|
|
973
|
+
setLoading(false);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
const allPages = result.data ?? [];
|
|
977
|
+
const sorted = [...allPages].sort((a, b) => {
|
|
978
|
+
const dateA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
|
|
979
|
+
const dateB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
|
|
980
|
+
return dateB - dateA;
|
|
981
|
+
});
|
|
982
|
+
setPages(sorted.slice(0, 10));
|
|
983
|
+
setLoading(false);
|
|
984
|
+
},
|
|
985
|
+
[api]
|
|
986
|
+
);
|
|
987
|
+
React8.useEffect(() => {
|
|
988
|
+
const signal = { cancelled: false };
|
|
989
|
+
void fetchPages(signal);
|
|
990
|
+
return () => {
|
|
991
|
+
signal.cancelled = true;
|
|
992
|
+
};
|
|
993
|
+
}, [fetchPages]);
|
|
994
|
+
return /* @__PURE__ */ jsxs7("div", { className: cn("space-y-3", className), "data-testid": "recent-pages", children: [
|
|
995
|
+
/* @__PURE__ */ jsx12("h2", { className: "text-lg font-semibold", children: "Recent Pages" }),
|
|
996
|
+
loading && /* @__PURE__ */ jsx12("div", { className: "space-y-2", "data-testid": "recent-pages-loading", children: Array.from({ length: 5 }).map((_, i) => (
|
|
997
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: Temporary loading skeletons with no stable data
|
|
998
|
+
/* @__PURE__ */ jsx12(Skeleton, { className: "h-10 w-full" }, i)
|
|
999
|
+
)) }),
|
|
1000
|
+
!loading && error && /* @__PURE__ */ jsxs7("div", { "data-testid": "recent-pages-error", children: [
|
|
1001
|
+
/* @__PURE__ */ jsx12("p", { className: "text-sm text-destructive", children: "Unable to load recent pages" }),
|
|
1002
|
+
/* @__PURE__ */ jsx12(
|
|
1003
|
+
Button,
|
|
1004
|
+
{
|
|
1005
|
+
type: "button",
|
|
1006
|
+
variant: "outline",
|
|
1007
|
+
size: "sm",
|
|
1008
|
+
className: "mt-2",
|
|
1009
|
+
onClick: () => void fetchPages(),
|
|
1010
|
+
"data-testid": "recent-pages-retry",
|
|
1011
|
+
children: "Retry"
|
|
1012
|
+
}
|
|
1013
|
+
)
|
|
1014
|
+
] }),
|
|
1015
|
+
!loading && !error && pages.length === 0 && /* @__PURE__ */ jsx12("p", { className: "text-sm text-muted-foreground py-4", "data-testid": "recent-pages-empty", children: "No pages yet." }),
|
|
1016
|
+
!loading && !error && pages.length > 0 && /* @__PURE__ */ jsx12("div", { className: "rounded-md border border-input", "data-testid": "recent-pages-list", children: pages.map((page) => /* @__PURE__ */ jsxs7(
|
|
1017
|
+
"button",
|
|
1018
|
+
{
|
|
1019
|
+
type: "button",
|
|
1020
|
+
className: "flex items-center justify-between border-b border-input last:border-0 px-3 py-2 hover:bg-muted/30 cursor-pointer w-full text-left",
|
|
1021
|
+
onClick: () => onSelectPage(page),
|
|
1022
|
+
"data-testid": `recent-page-${page.id}`,
|
|
1023
|
+
children: [
|
|
1024
|
+
/* @__PURE__ */ jsxs7("div", { className: "min-w-0 flex-1", children: [
|
|
1025
|
+
/* @__PURE__ */ jsx12("p", { className: "text-sm font-medium truncate", children: page.title }),
|
|
1026
|
+
/* @__PURE__ */ jsx12("p", { className: "text-xs text-muted-foreground truncate", children: page.slug })
|
|
1027
|
+
] }),
|
|
1028
|
+
page.updatedAt && /* @__PURE__ */ jsx12("span", { className: "ml-4 text-xs text-muted-foreground whitespace-nowrap", children: formatTimestamp(page.updatedAt) })
|
|
1029
|
+
]
|
|
1030
|
+
},
|
|
1031
|
+
page.id
|
|
1032
|
+
)) })
|
|
1033
|
+
] });
|
|
1034
|
+
}
|
|
1035
|
+
RecentPages.displayName = "RecentPages";
|
|
1036
|
+
|
|
1037
|
+
// src/components/dashboard/dashboard-page.tsx
|
|
1038
|
+
import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1039
|
+
function DashboardPage({
|
|
1040
|
+
onSelectPage,
|
|
1041
|
+
onCreatePage,
|
|
1042
|
+
onUploadMedia,
|
|
1043
|
+
className
|
|
1044
|
+
}) {
|
|
1045
|
+
return /* @__PURE__ */ jsxs8("div", { className: cn("space-y-6", className), "data-testid": "dashboard-page", children: [
|
|
1046
|
+
/* @__PURE__ */ jsx13("h1", { className: "text-2xl font-bold", children: "Dashboard" }),
|
|
1047
|
+
/* @__PURE__ */ jsx13(ErrorBoundary, { children: /* @__PURE__ */ jsx13(KpiCards, {}) }),
|
|
1048
|
+
/* @__PURE__ */ jsxs8("div", { className: "grid gap-6 md:grid-cols-3", children: [
|
|
1049
|
+
/* @__PURE__ */ jsx13("div", { className: "md:col-span-2", children: /* @__PURE__ */ jsx13(ErrorBoundary, { children: /* @__PURE__ */ jsx13(RecentPages, { onSelectPage }) }) }),
|
|
1050
|
+
/* @__PURE__ */ jsx13("div", { children: /* @__PURE__ */ jsx13(ErrorBoundary, { children: /* @__PURE__ */ jsx13(QuickActions, { onCreatePage, onUploadMedia }) }) })
|
|
1051
|
+
] })
|
|
1052
|
+
] });
|
|
1053
|
+
}
|
|
1054
|
+
DashboardPage.displayName = "DashboardPage";
|
|
1055
|
+
|
|
1056
|
+
// src/components/editors/page-editor.tsx
|
|
1057
|
+
import * as React20 from "react";
|
|
1058
|
+
|
|
1059
|
+
// src/lib/form-generator.tsx
|
|
1060
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
1061
|
+
import { getFieldMeta } from "@structcms/core";
|
|
1062
|
+
import * as React19 from "react";
|
|
1063
|
+
import { Controller, useForm } from "react-hook-form";
|
|
1064
|
+
|
|
1065
|
+
// src/components/inputs/array-field.tsx
|
|
1066
|
+
import * as React10 from "react";
|
|
1067
|
+
|
|
1068
|
+
// src/components/ui/label.tsx
|
|
1069
|
+
import { cva as cva2 } from "class-variance-authority";
|
|
1070
|
+
import * as React9 from "react";
|
|
1071
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
1072
|
+
var labelVariants = cva2(
|
|
1073
|
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
1074
|
+
);
|
|
1075
|
+
var Label = React9.forwardRef(({ className, ...props }, ref) => (
|
|
1076
|
+
// biome-ignore lint/a11y/noLabelWithoutControl: Generic label component, control association handled by consumers
|
|
1077
|
+
/* @__PURE__ */ jsx14("label", { ref, className: cn(labelVariants(), className), ...props })
|
|
1078
|
+
));
|
|
1079
|
+
Label.displayName = "Label";
|
|
1080
|
+
|
|
1081
|
+
// src/components/inputs/array-field.tsx
|
|
1082
|
+
import { jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1083
|
+
function ArrayFieldInner({
|
|
1084
|
+
label,
|
|
1085
|
+
value,
|
|
1086
|
+
onChange,
|
|
1087
|
+
renderItem,
|
|
1088
|
+
createDefaultItem,
|
|
1089
|
+
error,
|
|
1090
|
+
required,
|
|
1091
|
+
className,
|
|
1092
|
+
id,
|
|
1093
|
+
name
|
|
1094
|
+
}, ref) {
|
|
1095
|
+
const inputId = id || name || React10.useId();
|
|
1096
|
+
const handleAdd = () => {
|
|
1097
|
+
onChange([...value, createDefaultItem()]);
|
|
1098
|
+
};
|
|
1099
|
+
const handleRemove = (index) => {
|
|
1100
|
+
const newValue = [...value];
|
|
1101
|
+
newValue.splice(index, 1);
|
|
1102
|
+
onChange(newValue);
|
|
1103
|
+
};
|
|
1104
|
+
const handleMoveUp = (index) => {
|
|
1105
|
+
if (index === 0) return;
|
|
1106
|
+
const newValue = [...value];
|
|
1107
|
+
const temp = newValue[index];
|
|
1108
|
+
newValue[index] = newValue[index - 1];
|
|
1109
|
+
newValue[index - 1] = temp;
|
|
1110
|
+
onChange(newValue);
|
|
1111
|
+
};
|
|
1112
|
+
const handleMoveDown = (index) => {
|
|
1113
|
+
if (index === value.length - 1) return;
|
|
1114
|
+
const newValue = [...value];
|
|
1115
|
+
const temp = newValue[index];
|
|
1116
|
+
newValue[index] = newValue[index + 1];
|
|
1117
|
+
newValue[index + 1] = temp;
|
|
1118
|
+
onChange(newValue);
|
|
1119
|
+
};
|
|
1120
|
+
const handleItemChange = (index, item) => {
|
|
1121
|
+
const newValue = [...value];
|
|
1122
|
+
newValue[index] = item;
|
|
1123
|
+
onChange(newValue);
|
|
1124
|
+
};
|
|
1125
|
+
return /* @__PURE__ */ jsxs9("div", { ref, className: cn("space-y-2", className), "data-testid": "array-field", children: [
|
|
1126
|
+
/* @__PURE__ */ jsxs9(Label, { htmlFor: inputId, children: [
|
|
1127
|
+
label,
|
|
1128
|
+
required && /* @__PURE__ */ jsx15("span", { className: "text-destructive ml-1", children: "*" })
|
|
1129
|
+
] }),
|
|
1130
|
+
/* @__PURE__ */ jsxs9(
|
|
1131
|
+
"div",
|
|
1132
|
+
{
|
|
1133
|
+
className: cn(
|
|
1134
|
+
"rounded-md border border-input bg-background p-4",
|
|
1135
|
+
error && "border-destructive"
|
|
1136
|
+
),
|
|
1137
|
+
children: [
|
|
1138
|
+
value.length === 0 ? /* @__PURE__ */ jsx15("p", { className: "text-sm text-muted-foreground text-center py-4", children: "No items yet" }) : /* @__PURE__ */ jsx15("div", { className: "space-y-3", children: value.map((item, index) => /* @__PURE__ */ jsxs9(
|
|
1139
|
+
"div",
|
|
1140
|
+
{
|
|
1141
|
+
className: "flex gap-2 items-start p-3 rounded-md border border-input bg-muted/50",
|
|
1142
|
+
"data-testid": `array-item-${index}`,
|
|
1143
|
+
children: [
|
|
1144
|
+
/* @__PURE__ */ jsx15("div", { className: "flex-1", children: renderItem(item, index, (newItem) => handleItemChange(index, newItem)) }),
|
|
1145
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-1", children: [
|
|
1146
|
+
/* @__PURE__ */ jsx15(
|
|
1147
|
+
Button,
|
|
1148
|
+
{
|
|
1149
|
+
type: "button",
|
|
1150
|
+
variant: "ghost",
|
|
1151
|
+
size: "icon",
|
|
1152
|
+
onClick: () => handleMoveUp(index),
|
|
1153
|
+
disabled: index === 0,
|
|
1154
|
+
title: "Move up",
|
|
1155
|
+
"data-testid": `move-up-${index}`,
|
|
1156
|
+
children: "\u2191"
|
|
1157
|
+
}
|
|
1158
|
+
),
|
|
1159
|
+
/* @__PURE__ */ jsx15(
|
|
1160
|
+
Button,
|
|
1161
|
+
{
|
|
1162
|
+
type: "button",
|
|
1163
|
+
variant: "ghost",
|
|
1164
|
+
size: "icon",
|
|
1165
|
+
onClick: () => handleMoveDown(index),
|
|
1166
|
+
disabled: index === value.length - 1,
|
|
1167
|
+
title: "Move down",
|
|
1168
|
+
"data-testid": `move-down-${index}`,
|
|
1169
|
+
children: "\u2193"
|
|
1170
|
+
}
|
|
1171
|
+
),
|
|
1172
|
+
/* @__PURE__ */ jsx15(
|
|
1173
|
+
Button,
|
|
1174
|
+
{
|
|
1175
|
+
type: "button",
|
|
1176
|
+
variant: "ghost",
|
|
1177
|
+
size: "icon",
|
|
1178
|
+
onClick: () => handleRemove(index),
|
|
1179
|
+
title: "Remove",
|
|
1180
|
+
"data-testid": `remove-${index}`,
|
|
1181
|
+
children: "\u2715"
|
|
1182
|
+
}
|
|
1183
|
+
)
|
|
1184
|
+
] })
|
|
1185
|
+
]
|
|
1186
|
+
},
|
|
1187
|
+
index
|
|
1188
|
+
)) }),
|
|
1189
|
+
/* @__PURE__ */ jsx15("div", { className: "mt-4", children: /* @__PURE__ */ jsx15(
|
|
1190
|
+
Button,
|
|
1191
|
+
{
|
|
1192
|
+
type: "button",
|
|
1193
|
+
variant: "outline",
|
|
1194
|
+
onClick: handleAdd,
|
|
1195
|
+
id: inputId,
|
|
1196
|
+
"data-testid": "add-item",
|
|
1197
|
+
children: "Add Item"
|
|
1198
|
+
}
|
|
1199
|
+
) })
|
|
1200
|
+
]
|
|
1201
|
+
}
|
|
1202
|
+
),
|
|
1203
|
+
error && /* @__PURE__ */ jsx15("p", { id: `${inputId}-error`, className: "text-sm text-destructive", children: error })
|
|
1204
|
+
] });
|
|
1205
|
+
}
|
|
1206
|
+
var ArrayField = React10.forwardRef(ArrayFieldInner);
|
|
1207
|
+
|
|
1208
|
+
// src/components/inputs/image-picker.tsx
|
|
1209
|
+
import * as React13 from "react";
|
|
1210
|
+
|
|
1211
|
+
// src/components/media/media-browser.tsx
|
|
1212
|
+
import * as React11 from "react";
|
|
1213
|
+
import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1214
|
+
function MediaBrowser({ onSelect, className, pageSize = 12 }) {
|
|
1215
|
+
const api = useApiClient();
|
|
1216
|
+
const [items, setItems] = React11.useState([]);
|
|
1217
|
+
const [loading, setLoading] = React11.useState(true);
|
|
1218
|
+
const [error, setError] = React11.useState(null);
|
|
1219
|
+
const [hasMore, setHasMore] = React11.useState(false);
|
|
1220
|
+
const [page, setPage] = React11.useState(0);
|
|
1221
|
+
const fileInputRef = React11.useRef(null);
|
|
1222
|
+
const fetchMedia = React11.useCallback(
|
|
1223
|
+
async (pageNum, append) => {
|
|
1224
|
+
setLoading(true);
|
|
1225
|
+
setError(null);
|
|
1226
|
+
const result = await api.get(
|
|
1227
|
+
`/media?limit=${pageSize}&offset=${pageNum * pageSize}`
|
|
1228
|
+
);
|
|
1229
|
+
if (result.error) {
|
|
1230
|
+
setError(result.error.message);
|
|
1231
|
+
setLoading(false);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
const newItems = result.data ?? [];
|
|
1235
|
+
setItems((prev) => append ? [...prev, ...newItems] : newItems);
|
|
1236
|
+
setHasMore(newItems.length >= pageSize);
|
|
1237
|
+
setLoading(false);
|
|
1238
|
+
},
|
|
1239
|
+
[api, pageSize]
|
|
1240
|
+
);
|
|
1241
|
+
React11.useEffect(() => {
|
|
1242
|
+
void fetchMedia(0, false);
|
|
1243
|
+
}, [fetchMedia]);
|
|
1244
|
+
const handleLoadMore = () => {
|
|
1245
|
+
const nextPage = page + 1;
|
|
1246
|
+
setPage(nextPage);
|
|
1247
|
+
void fetchMedia(nextPage, true);
|
|
1248
|
+
};
|
|
1249
|
+
const handleUpload = async (e) => {
|
|
1250
|
+
const file = e.target.files?.[0];
|
|
1251
|
+
if (!file) return;
|
|
1252
|
+
const formData = new FormData();
|
|
1253
|
+
formData.append("file", file);
|
|
1254
|
+
setLoading(true);
|
|
1255
|
+
setError(null);
|
|
1256
|
+
const result = await api.upload("/media", formData);
|
|
1257
|
+
if (result.error) {
|
|
1258
|
+
setError(result.error.message);
|
|
1259
|
+
setLoading(false);
|
|
1260
|
+
} else {
|
|
1261
|
+
setPage(0);
|
|
1262
|
+
await fetchMedia(0, false);
|
|
1263
|
+
}
|
|
1264
|
+
if (fileInputRef.current) {
|
|
1265
|
+
fileInputRef.current.value = "";
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
const handleDelete = async (item) => {
|
|
1269
|
+
const result = await api.delete(`/media/${item.id}`);
|
|
1270
|
+
if (result.error) {
|
|
1271
|
+
setError(result.error.message);
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
setItems((prev) => prev.filter((i) => i.id !== item.id));
|
|
1275
|
+
};
|
|
1276
|
+
return /* @__PURE__ */ jsxs10("div", { className: cn("space-y-4", className), "data-testid": "media-browser", children: [
|
|
1277
|
+
/* @__PURE__ */ jsxs10("div", { className: "flex items-center justify-between", children: [
|
|
1278
|
+
/* @__PURE__ */ jsx16("h2", { className: "text-xl font-semibold", children: "Media" }),
|
|
1279
|
+
/* @__PURE__ */ jsxs10("div", { children: [
|
|
1280
|
+
/* @__PURE__ */ jsx16(
|
|
1281
|
+
"input",
|
|
1282
|
+
{
|
|
1283
|
+
ref: fileInputRef,
|
|
1284
|
+
type: "file",
|
|
1285
|
+
accept: "image/*",
|
|
1286
|
+
className: "hidden",
|
|
1287
|
+
onChange: (e) => void handleUpload(e),
|
|
1288
|
+
"data-testid": "file-input"
|
|
1289
|
+
}
|
|
1290
|
+
),
|
|
1291
|
+
/* @__PURE__ */ jsx16(
|
|
1292
|
+
Button,
|
|
1293
|
+
{
|
|
1294
|
+
type: "button",
|
|
1295
|
+
onClick: () => fileInputRef.current?.click(),
|
|
1296
|
+
"data-testid": "upload-button",
|
|
1297
|
+
children: "Upload"
|
|
1298
|
+
}
|
|
1299
|
+
)
|
|
1300
|
+
] })
|
|
1301
|
+
] }),
|
|
1302
|
+
error && /* @__PURE__ */ jsx16("p", { className: "text-sm text-destructive", "data-testid": "error", children: error }),
|
|
1303
|
+
!loading && items.length === 0 && !error && /* @__PURE__ */ jsx16("p", { className: "text-sm text-muted-foreground text-center py-8", "data-testid": "empty-state", children: "No media files yet. Upload your first file." }),
|
|
1304
|
+
items.length > 0 && /* @__PURE__ */ jsx16(
|
|
1305
|
+
"div",
|
|
1306
|
+
{
|
|
1307
|
+
className: "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4",
|
|
1308
|
+
"data-testid": "media-grid",
|
|
1309
|
+
children: items.map((item) => /* @__PURE__ */ jsxs10(
|
|
1310
|
+
"div",
|
|
1311
|
+
{
|
|
1312
|
+
className: "group relative rounded-md border border-input bg-background overflow-hidden",
|
|
1313
|
+
"data-testid": `media-item-${item.id}`,
|
|
1314
|
+
children: [
|
|
1315
|
+
/* @__PURE__ */ jsx16(
|
|
1316
|
+
"button",
|
|
1317
|
+
{
|
|
1318
|
+
type: "button",
|
|
1319
|
+
className: "w-full aspect-square bg-muted flex items-center justify-center overflow-hidden cursor-pointer",
|
|
1320
|
+
onClick: () => onSelect?.(item),
|
|
1321
|
+
"data-testid": `media-select-${item.id}`,
|
|
1322
|
+
children: /* @__PURE__ */ jsx16("img", { src: item.url, alt: item.filename, className: "h-full w-full object-cover" })
|
|
1323
|
+
}
|
|
1324
|
+
),
|
|
1325
|
+
/* @__PURE__ */ jsxs10("div", { className: "p-2 flex items-center justify-between", children: [
|
|
1326
|
+
/* @__PURE__ */ jsx16("p", { className: "text-xs text-muted-foreground truncate flex-1", children: item.filename }),
|
|
1327
|
+
/* @__PURE__ */ jsx16(
|
|
1328
|
+
Button,
|
|
1329
|
+
{
|
|
1330
|
+
type: "button",
|
|
1331
|
+
variant: "ghost",
|
|
1332
|
+
size: "icon",
|
|
1333
|
+
className: "h-6 w-6",
|
|
1334
|
+
onClick: () => void handleDelete(item),
|
|
1335
|
+
title: "Delete",
|
|
1336
|
+
"data-testid": `media-delete-${item.id}`,
|
|
1337
|
+
children: "\u2715"
|
|
1338
|
+
}
|
|
1339
|
+
)
|
|
1340
|
+
] })
|
|
1341
|
+
]
|
|
1342
|
+
},
|
|
1343
|
+
item.id
|
|
1344
|
+
))
|
|
1345
|
+
}
|
|
1346
|
+
),
|
|
1347
|
+
loading && /* @__PURE__ */ jsx16("p", { className: "text-sm text-muted-foreground", "data-testid": "loading", children: "Loading media..." }),
|
|
1348
|
+
hasMore && !loading && /* @__PURE__ */ jsx16("div", { className: "text-center", children: /* @__PURE__ */ jsx16(Button, { type: "button", variant: "outline", onClick: handleLoadMore, "data-testid": "load-more", children: "Load More" }) })
|
|
1349
|
+
] });
|
|
1350
|
+
}
|
|
1351
|
+
MediaBrowser.displayName = "MediaBrowser";
|
|
1352
|
+
|
|
1353
|
+
// src/components/ui/dialog.tsx
|
|
1354
|
+
import * as React12 from "react";
|
|
1355
|
+
import { createPortal } from "react-dom";
|
|
1356
|
+
import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1357
|
+
function Dialog({ open, onClose, children, className, title }) {
|
|
1358
|
+
const overlayRef = React12.useRef(null);
|
|
1359
|
+
React12.useEffect(() => {
|
|
1360
|
+
if (!open) return;
|
|
1361
|
+
const handleKeyDown = (e) => {
|
|
1362
|
+
if (e.key === "Escape") {
|
|
1363
|
+
onClose();
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
1367
|
+
document.body.style.overflow = "hidden";
|
|
1368
|
+
return () => {
|
|
1369
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
1370
|
+
document.body.style.overflow = "";
|
|
1371
|
+
};
|
|
1372
|
+
}, [open, onClose]);
|
|
1373
|
+
const handleOverlayClick = (e) => {
|
|
1374
|
+
if (e.target === overlayRef.current) {
|
|
1375
|
+
onClose();
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
if (!open) return null;
|
|
1379
|
+
return createPortal(
|
|
1380
|
+
/* @__PURE__ */ jsx17(
|
|
1381
|
+
"div",
|
|
1382
|
+
{
|
|
1383
|
+
ref: overlayRef,
|
|
1384
|
+
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50",
|
|
1385
|
+
onClick: handleOverlayClick,
|
|
1386
|
+
onKeyDown: (e) => {
|
|
1387
|
+
if (e.key === "Enter" || e.key === "Escape") {
|
|
1388
|
+
e.preventDefault();
|
|
1389
|
+
onClose();
|
|
1390
|
+
}
|
|
1391
|
+
},
|
|
1392
|
+
role: "button",
|
|
1393
|
+
tabIndex: -1,
|
|
1394
|
+
"data-testid": "dialog-overlay",
|
|
1395
|
+
children: /* @__PURE__ */ jsxs11(
|
|
1396
|
+
"div",
|
|
1397
|
+
{
|
|
1398
|
+
className: cn(
|
|
1399
|
+
"relative mx-4 max-h-[85vh] w-full max-w-3xl overflow-auto rounded-lg border border-input bg-background p-6 shadow-lg",
|
|
1400
|
+
className
|
|
1401
|
+
),
|
|
1402
|
+
role: "dialog",
|
|
1403
|
+
"aria-modal": "true",
|
|
1404
|
+
"aria-label": title,
|
|
1405
|
+
"data-testid": "dialog-content",
|
|
1406
|
+
children: [
|
|
1407
|
+
title && /* @__PURE__ */ jsxs11("div", { className: "mb-4 flex items-center justify-between", children: [
|
|
1408
|
+
/* @__PURE__ */ jsx17("h2", { className: "text-lg font-semibold", children: title }),
|
|
1409
|
+
/* @__PURE__ */ jsx17(
|
|
1410
|
+
"button",
|
|
1411
|
+
{
|
|
1412
|
+
type: "button",
|
|
1413
|
+
onClick: onClose,
|
|
1414
|
+
className: "rounded-sm p-1 text-muted-foreground hover:text-foreground",
|
|
1415
|
+
"aria-label": "Close",
|
|
1416
|
+
"data-testid": "dialog-close",
|
|
1417
|
+
children: "\u2715"
|
|
1418
|
+
}
|
|
1419
|
+
)
|
|
1420
|
+
] }),
|
|
1421
|
+
children
|
|
1422
|
+
]
|
|
1423
|
+
}
|
|
1424
|
+
)
|
|
1425
|
+
}
|
|
1426
|
+
),
|
|
1427
|
+
document.body
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
Dialog.displayName = "Dialog";
|
|
1431
|
+
|
|
1432
|
+
// src/components/inputs/image-picker.tsx
|
|
1433
|
+
import { jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1434
|
+
function ImagePicker({
|
|
1435
|
+
label,
|
|
1436
|
+
value,
|
|
1437
|
+
onChange,
|
|
1438
|
+
onBrowse,
|
|
1439
|
+
error,
|
|
1440
|
+
required,
|
|
1441
|
+
className,
|
|
1442
|
+
id,
|
|
1443
|
+
name
|
|
1444
|
+
}) {
|
|
1445
|
+
const inputId = id || name || React13.useId();
|
|
1446
|
+
const [mediaBrowserOpen, setMediaBrowserOpen] = React13.useState(false);
|
|
1447
|
+
const handleClear = () => {
|
|
1448
|
+
onChange?.("");
|
|
1449
|
+
};
|
|
1450
|
+
const handleBrowse = onBrowse ?? (() => setMediaBrowserOpen(true));
|
|
1451
|
+
const handleMediaSelect = (item) => {
|
|
1452
|
+
onChange?.(item.url);
|
|
1453
|
+
setMediaBrowserOpen(false);
|
|
1454
|
+
};
|
|
1455
|
+
return /* @__PURE__ */ jsxs12("div", { className: cn("space-y-2", className), children: [
|
|
1456
|
+
/* @__PURE__ */ jsxs12(Label, { htmlFor: inputId, children: [
|
|
1457
|
+
label,
|
|
1458
|
+
required && /* @__PURE__ */ jsx18("span", { className: "text-destructive ml-1", children: "*" })
|
|
1459
|
+
] }),
|
|
1460
|
+
/* @__PURE__ */ jsx18(
|
|
1461
|
+
"div",
|
|
1462
|
+
{
|
|
1463
|
+
className: cn(
|
|
1464
|
+
"rounded-md border border-input bg-background p-4",
|
|
1465
|
+
error && "border-destructive"
|
|
1466
|
+
),
|
|
1467
|
+
children: value ? /* @__PURE__ */ jsxs12("div", { className: "space-y-3", children: [
|
|
1468
|
+
/* @__PURE__ */ jsx18("div", { className: "relative aspect-video w-full max-w-xs overflow-hidden rounded-md bg-muted", children: /* @__PURE__ */ jsx18(
|
|
1469
|
+
"img",
|
|
1470
|
+
{
|
|
1471
|
+
src: value,
|
|
1472
|
+
alt: "Preview",
|
|
1473
|
+
className: "h-full w-full object-cover",
|
|
1474
|
+
"data-testid": "image-preview"
|
|
1475
|
+
}
|
|
1476
|
+
) }),
|
|
1477
|
+
/* @__PURE__ */ jsxs12("div", { className: "flex gap-2", children: [
|
|
1478
|
+
/* @__PURE__ */ jsx18(Button, { type: "button", variant: "outline", size: "sm", onClick: handleBrowse, children: "Change" }),
|
|
1479
|
+
/* @__PURE__ */ jsx18(
|
|
1480
|
+
Button,
|
|
1481
|
+
{
|
|
1482
|
+
type: "button",
|
|
1483
|
+
variant: "outline",
|
|
1484
|
+
size: "sm",
|
|
1485
|
+
onClick: handleClear,
|
|
1486
|
+
"data-testid": "clear-button",
|
|
1487
|
+
children: "Clear"
|
|
1488
|
+
}
|
|
1489
|
+
)
|
|
1490
|
+
] })
|
|
1491
|
+
] }) : /* @__PURE__ */ jsxs12("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: [
|
|
1492
|
+
/* @__PURE__ */ jsx18("div", { className: "mb-4 text-4xl text-muted-foreground", children: "\u{1F5BC}\uFE0F" }),
|
|
1493
|
+
/* @__PURE__ */ jsx18("p", { className: "mb-4 text-sm text-muted-foreground", children: "No image selected" }),
|
|
1494
|
+
/* @__PURE__ */ jsx18(
|
|
1495
|
+
Button,
|
|
1496
|
+
{
|
|
1497
|
+
type: "button",
|
|
1498
|
+
variant: "outline",
|
|
1499
|
+
onClick: handleBrowse,
|
|
1500
|
+
id: inputId,
|
|
1501
|
+
"data-testid": "browse-button",
|
|
1502
|
+
children: "Browse Media"
|
|
1503
|
+
}
|
|
1504
|
+
)
|
|
1505
|
+
] })
|
|
1506
|
+
}
|
|
1507
|
+
),
|
|
1508
|
+
error && /* @__PURE__ */ jsx18("p", { id: `${inputId}-error`, className: "text-sm text-destructive", children: error }),
|
|
1509
|
+
!onBrowse && /* @__PURE__ */ jsx18(
|
|
1510
|
+
Dialog,
|
|
1511
|
+
{
|
|
1512
|
+
open: mediaBrowserOpen,
|
|
1513
|
+
onClose: () => setMediaBrowserOpen(false),
|
|
1514
|
+
title: "Select Media",
|
|
1515
|
+
children: /* @__PURE__ */ jsx18(MediaBrowser, { onSelect: handleMediaSelect })
|
|
1516
|
+
}
|
|
1517
|
+
)
|
|
1518
|
+
] });
|
|
1519
|
+
}
|
|
1520
|
+
ImagePicker.displayName = "ImagePicker";
|
|
1521
|
+
|
|
1522
|
+
// src/components/inputs/object-field.tsx
|
|
1523
|
+
import * as React14 from "react";
|
|
1524
|
+
import { jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
1525
|
+
function ObjectField({ label, children, error, required, className, id, name }) {
|
|
1526
|
+
const fieldId = id || name || React14.useId();
|
|
1527
|
+
return /* @__PURE__ */ jsxs13("div", { className: cn("space-y-2", className), children: [
|
|
1528
|
+
/* @__PURE__ */ jsxs13(Label, { htmlFor: fieldId, children: [
|
|
1529
|
+
label,
|
|
1530
|
+
required && /* @__PURE__ */ jsx19("span", { className: "text-destructive ml-1", children: "*" })
|
|
1531
|
+
] }),
|
|
1532
|
+
/* @__PURE__ */ jsx19(
|
|
1533
|
+
"div",
|
|
1534
|
+
{
|
|
1535
|
+
id: fieldId,
|
|
1536
|
+
className: cn(
|
|
1537
|
+
"rounded-md border border-input bg-muted/30 p-4 space-y-4",
|
|
1538
|
+
error && "border-destructive"
|
|
1539
|
+
),
|
|
1540
|
+
role: "group",
|
|
1541
|
+
"aria-labelledby": `${fieldId}-label`,
|
|
1542
|
+
"aria-describedby": error ? `${fieldId}-error` : void 0,
|
|
1543
|
+
"data-testid": "object-field-container",
|
|
1544
|
+
children
|
|
1545
|
+
}
|
|
1546
|
+
),
|
|
1547
|
+
error && /* @__PURE__ */ jsx19("p", { id: `${fieldId}-error`, className: "text-sm text-destructive", children: error })
|
|
1548
|
+
] });
|
|
1549
|
+
}
|
|
1550
|
+
ObjectField.displayName = "ObjectField";
|
|
1551
|
+
|
|
1552
|
+
// src/components/inputs/rich-text-editor.tsx
|
|
1553
|
+
import Link from "@tiptap/extension-link";
|
|
1554
|
+
import { EditorContent, useEditor } from "@tiptap/react";
|
|
1555
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
1556
|
+
import * as React15 from "react";
|
|
1557
|
+
import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
1558
|
+
function ToolbarButton({ onClick, isActive, disabled, children, title }) {
|
|
1559
|
+
return /* @__PURE__ */ jsx20(
|
|
1560
|
+
"button",
|
|
1561
|
+
{
|
|
1562
|
+
type: "button",
|
|
1563
|
+
onClick,
|
|
1564
|
+
disabled,
|
|
1565
|
+
title,
|
|
1566
|
+
className: cn(
|
|
1567
|
+
"px-2 py-1 text-sm rounded hover:bg-accent disabled:opacity-50 disabled:cursor-not-allowed",
|
|
1568
|
+
isActive && "bg-accent text-accent-foreground"
|
|
1569
|
+
),
|
|
1570
|
+
children
|
|
1571
|
+
}
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
function RichTextEditor({
|
|
1575
|
+
label,
|
|
1576
|
+
value = "",
|
|
1577
|
+
onChange,
|
|
1578
|
+
error,
|
|
1579
|
+
required,
|
|
1580
|
+
placeholder,
|
|
1581
|
+
className,
|
|
1582
|
+
id,
|
|
1583
|
+
name
|
|
1584
|
+
}) {
|
|
1585
|
+
const inputId = id || name || React15.useId();
|
|
1586
|
+
const editor = useEditor({
|
|
1587
|
+
extensions: [
|
|
1588
|
+
StarterKit.configure({
|
|
1589
|
+
heading: {
|
|
1590
|
+
levels: [1, 2, 3]
|
|
1591
|
+
}
|
|
1592
|
+
}),
|
|
1593
|
+
Link.extend({ name: "customLink" }).configure({
|
|
1594
|
+
openOnClick: false,
|
|
1595
|
+
HTMLAttributes: {
|
|
1596
|
+
class: "text-primary underline"
|
|
1597
|
+
}
|
|
1598
|
+
})
|
|
1599
|
+
],
|
|
1600
|
+
content: value,
|
|
1601
|
+
immediatelyRender: false,
|
|
1602
|
+
editorProps: {
|
|
1603
|
+
attributes: {
|
|
1604
|
+
class: "prose prose-sm max-w-none min-h-[150px] p-3 focus:outline-none",
|
|
1605
|
+
"aria-invalid": error ? "true" : "false",
|
|
1606
|
+
...error ? { "aria-describedby": `${inputId}-error` } : {}
|
|
1607
|
+
}
|
|
1608
|
+
},
|
|
1609
|
+
onUpdate: ({ editor: updatedEditor }) => {
|
|
1610
|
+
onChange?.(updatedEditor.getHTML());
|
|
1611
|
+
}
|
|
1612
|
+
});
|
|
1613
|
+
const setLink = React15.useCallback(() => {
|
|
1614
|
+
if (!editor) return;
|
|
1615
|
+
const previousUrl = editor.getAttributes("link").href;
|
|
1616
|
+
const url = window.prompt("URL", previousUrl);
|
|
1617
|
+
if (url === null) {
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
if (url === "") {
|
|
1621
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
|
|
1625
|
+
}, [editor]);
|
|
1626
|
+
if (!editor) {
|
|
1627
|
+
return null;
|
|
1628
|
+
}
|
|
1629
|
+
return /* @__PURE__ */ jsxs14("div", { className: cn("space-y-2", className), children: [
|
|
1630
|
+
/* @__PURE__ */ jsxs14(Label, { htmlFor: inputId, children: [
|
|
1631
|
+
label,
|
|
1632
|
+
required && /* @__PURE__ */ jsx20("span", { className: "text-destructive ml-1", children: "*" })
|
|
1633
|
+
] }),
|
|
1634
|
+
/* @__PURE__ */ jsxs14(
|
|
1635
|
+
"div",
|
|
1636
|
+
{
|
|
1637
|
+
className: cn(
|
|
1638
|
+
"rounded-md border border-input bg-background",
|
|
1639
|
+
error && "border-destructive"
|
|
1640
|
+
),
|
|
1641
|
+
children: [
|
|
1642
|
+
/* @__PURE__ */ jsxs14("div", { className: "flex flex-wrap gap-1 border-b border-input p-2", children: [
|
|
1643
|
+
/* @__PURE__ */ jsx20(
|
|
1644
|
+
ToolbarButton,
|
|
1645
|
+
{
|
|
1646
|
+
onClick: () => editor.chain().focus().toggleBold().run(),
|
|
1647
|
+
isActive: editor.isActive("bold"),
|
|
1648
|
+
disabled: !editor.can().chain().focus().toggleBold().run(),
|
|
1649
|
+
title: "Bold",
|
|
1650
|
+
children: /* @__PURE__ */ jsx20("strong", { children: "B" })
|
|
1651
|
+
}
|
|
1652
|
+
),
|
|
1653
|
+
/* @__PURE__ */ jsx20(
|
|
1654
|
+
ToolbarButton,
|
|
1655
|
+
{
|
|
1656
|
+
onClick: () => editor.chain().focus().toggleItalic().run(),
|
|
1657
|
+
isActive: editor.isActive("italic"),
|
|
1658
|
+
disabled: !editor.can().chain().focus().toggleItalic().run(),
|
|
1659
|
+
title: "Italic",
|
|
1660
|
+
children: /* @__PURE__ */ jsx20("em", { children: "I" })
|
|
1661
|
+
}
|
|
1662
|
+
),
|
|
1663
|
+
/* @__PURE__ */ jsx20(ToolbarButton, { onClick: setLink, isActive: editor.isActive("link"), title: "Link", children: "\u{1F517}" }),
|
|
1664
|
+
/* @__PURE__ */ jsx20("div", { className: "w-px bg-border mx-1" }),
|
|
1665
|
+
/* @__PURE__ */ jsx20(
|
|
1666
|
+
ToolbarButton,
|
|
1667
|
+
{
|
|
1668
|
+
onClick: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
|
1669
|
+
isActive: editor.isActive("heading", { level: 1 }),
|
|
1670
|
+
title: "Heading 1",
|
|
1671
|
+
children: "H1"
|
|
1672
|
+
}
|
|
1673
|
+
),
|
|
1674
|
+
/* @__PURE__ */ jsx20(
|
|
1675
|
+
ToolbarButton,
|
|
1676
|
+
{
|
|
1677
|
+
onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
|
1678
|
+
isActive: editor.isActive("heading", { level: 2 }),
|
|
1679
|
+
title: "Heading 2",
|
|
1680
|
+
children: "H2"
|
|
1681
|
+
}
|
|
1682
|
+
),
|
|
1683
|
+
/* @__PURE__ */ jsx20(
|
|
1684
|
+
ToolbarButton,
|
|
1685
|
+
{
|
|
1686
|
+
onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
|
1687
|
+
isActive: editor.isActive("heading", { level: 3 }),
|
|
1688
|
+
title: "Heading 3",
|
|
1689
|
+
children: "H3"
|
|
1690
|
+
}
|
|
1691
|
+
),
|
|
1692
|
+
/* @__PURE__ */ jsx20("div", { className: "w-px bg-border mx-1" }),
|
|
1693
|
+
/* @__PURE__ */ jsx20(
|
|
1694
|
+
ToolbarButton,
|
|
1695
|
+
{
|
|
1696
|
+
onClick: () => editor.chain().focus().toggleBulletList().run(),
|
|
1697
|
+
isActive: editor.isActive("bulletList"),
|
|
1698
|
+
title: "Bullet List",
|
|
1699
|
+
children: "\u2022"
|
|
1700
|
+
}
|
|
1701
|
+
),
|
|
1702
|
+
/* @__PURE__ */ jsx20(
|
|
1703
|
+
ToolbarButton,
|
|
1704
|
+
{
|
|
1705
|
+
onClick: () => editor.chain().focus().toggleOrderedList().run(),
|
|
1706
|
+
isActive: editor.isActive("orderedList"),
|
|
1707
|
+
title: "Ordered List",
|
|
1708
|
+
children: "1."
|
|
1709
|
+
}
|
|
1710
|
+
)
|
|
1711
|
+
] }),
|
|
1712
|
+
/* @__PURE__ */ jsx20(EditorContent, { editor, id: inputId, "data-placeholder": placeholder })
|
|
1713
|
+
]
|
|
1714
|
+
}
|
|
1715
|
+
),
|
|
1716
|
+
error && /* @__PURE__ */ jsx20("p", { id: `${inputId}-error`, className: "text-sm text-destructive", children: error })
|
|
1717
|
+
] });
|
|
1718
|
+
}
|
|
1719
|
+
RichTextEditor.displayName = "RichTextEditor";
|
|
1720
|
+
|
|
1721
|
+
// src/components/inputs/string-input.tsx
|
|
1722
|
+
import * as React16 from "react";
|
|
1723
|
+
import { jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
1724
|
+
var StringInput = React16.forwardRef(
|
|
1725
|
+
({ className, label, error, required, id, ...props }, ref) => {
|
|
1726
|
+
const inputId = id || props.name || React16.useId();
|
|
1727
|
+
return /* @__PURE__ */ jsxs15("div", { className: cn("space-y-2", className), children: [
|
|
1728
|
+
/* @__PURE__ */ jsxs15(Label, { htmlFor: inputId, children: [
|
|
1729
|
+
label,
|
|
1730
|
+
required && /* @__PURE__ */ jsx21("span", { className: "text-destructive ml-1", children: "*" })
|
|
1731
|
+
] }),
|
|
1732
|
+
/* @__PURE__ */ jsx21(
|
|
1733
|
+
Input,
|
|
1734
|
+
{
|
|
1735
|
+
id: inputId,
|
|
1736
|
+
ref,
|
|
1737
|
+
type: "text",
|
|
1738
|
+
"aria-invalid": !!error,
|
|
1739
|
+
"aria-describedby": error ? `${inputId}-error` : void 0,
|
|
1740
|
+
className: cn(error && "border-destructive"),
|
|
1741
|
+
...props
|
|
1742
|
+
}
|
|
1743
|
+
),
|
|
1744
|
+
error && /* @__PURE__ */ jsx21("p", { id: `${inputId}-error`, className: "text-sm text-destructive", children: error })
|
|
1745
|
+
] });
|
|
1746
|
+
}
|
|
1747
|
+
);
|
|
1748
|
+
StringInput.displayName = "StringInput";
|
|
1749
|
+
|
|
1750
|
+
// src/components/inputs/text-input.tsx
|
|
1751
|
+
import * as React18 from "react";
|
|
1752
|
+
|
|
1753
|
+
// src/components/ui/textarea.tsx
|
|
1754
|
+
import * as React17 from "react";
|
|
1755
|
+
import { jsx as jsx22 } from "react/jsx-runtime";
|
|
1756
|
+
var Textarea = React17.forwardRef(
|
|
1757
|
+
({ className, ...props }, ref) => {
|
|
1758
|
+
return /* @__PURE__ */ jsx22(
|
|
1759
|
+
"textarea",
|
|
1760
|
+
{
|
|
1761
|
+
className: cn(
|
|
1762
|
+
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
1763
|
+
className
|
|
1764
|
+
),
|
|
1765
|
+
ref,
|
|
1766
|
+
...props
|
|
1767
|
+
}
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
);
|
|
1771
|
+
Textarea.displayName = "Textarea";
|
|
1772
|
+
|
|
1773
|
+
// src/components/inputs/text-input.tsx
|
|
1774
|
+
import { jsx as jsx23, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
1775
|
+
var TextInput = React18.forwardRef(
|
|
1776
|
+
({ className, label, error, required, id, rows = 3, ...props }, ref) => {
|
|
1777
|
+
const inputId = id || props.name || React18.useId();
|
|
1778
|
+
return /* @__PURE__ */ jsxs16("div", { className: cn("space-y-2", className), children: [
|
|
1779
|
+
/* @__PURE__ */ jsxs16(Label, { htmlFor: inputId, children: [
|
|
1780
|
+
label,
|
|
1781
|
+
required && /* @__PURE__ */ jsx23("span", { className: "text-destructive ml-1", children: "*" })
|
|
1782
|
+
] }),
|
|
1783
|
+
/* @__PURE__ */ jsx23(
|
|
1784
|
+
Textarea,
|
|
1785
|
+
{
|
|
1786
|
+
id: inputId,
|
|
1787
|
+
ref,
|
|
1788
|
+
rows,
|
|
1789
|
+
"aria-invalid": !!error,
|
|
1790
|
+
"aria-describedby": error ? `${inputId}-error` : void 0,
|
|
1791
|
+
className: cn(error && "border-destructive"),
|
|
1792
|
+
...props
|
|
1793
|
+
}
|
|
1794
|
+
),
|
|
1795
|
+
error && /* @__PURE__ */ jsx23("p", { id: `${inputId}-error`, className: "text-sm text-destructive", children: error })
|
|
1796
|
+
] });
|
|
1797
|
+
}
|
|
1798
|
+
);
|
|
1799
|
+
TextInput.displayName = "TextInput";
|
|
1800
|
+
|
|
1801
|
+
// src/lib/form-generator.tsx
|
|
1802
|
+
import { jsx as jsx24, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
1803
|
+
function unwrapSchema(schema) {
|
|
1804
|
+
if ("unwrap" in schema && typeof schema.unwrap === "function") {
|
|
1805
|
+
return unwrapSchema(schema.unwrap());
|
|
1806
|
+
}
|
|
1807
|
+
if ("_def" in schema) {
|
|
1808
|
+
const def = schema._def;
|
|
1809
|
+
if ("innerType" in def && def.innerType) {
|
|
1810
|
+
return unwrapSchema(def.innerType);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
return schema;
|
|
1814
|
+
}
|
|
1815
|
+
function resolveFieldType(schema) {
|
|
1816
|
+
const meta = getFieldMeta(schema);
|
|
1817
|
+
if (meta) return meta.fieldType;
|
|
1818
|
+
const unwrapped = unwrapSchema(schema);
|
|
1819
|
+
const unwrappedMeta = getFieldMeta(unwrapped);
|
|
1820
|
+
return unwrappedMeta?.fieldType ?? null;
|
|
1821
|
+
}
|
|
1822
|
+
function fieldNameToLabel(name) {
|
|
1823
|
+
return name.replace(/([A-Z])/g, " $1").replace(/[_-]/g, " ").replace(/^\w/, (c) => c.toUpperCase()).trim();
|
|
1824
|
+
}
|
|
1825
|
+
function FormGenerator({
|
|
1826
|
+
schema,
|
|
1827
|
+
onSubmit,
|
|
1828
|
+
onChange,
|
|
1829
|
+
defaultValues,
|
|
1830
|
+
submitLabel = "Submit",
|
|
1831
|
+
className
|
|
1832
|
+
}) {
|
|
1833
|
+
const {
|
|
1834
|
+
register,
|
|
1835
|
+
handleSubmit,
|
|
1836
|
+
control,
|
|
1837
|
+
watch,
|
|
1838
|
+
formState: { errors }
|
|
1839
|
+
} = useForm({
|
|
1840
|
+
resolver: zodResolver(schema),
|
|
1841
|
+
defaultValues
|
|
1842
|
+
});
|
|
1843
|
+
React19.useEffect(() => {
|
|
1844
|
+
if (!onChange) return;
|
|
1845
|
+
const subscription = watch((values) => {
|
|
1846
|
+
onChange(values);
|
|
1847
|
+
});
|
|
1848
|
+
return () => subscription.unsubscribe();
|
|
1849
|
+
}, [watch, onChange]);
|
|
1850
|
+
const shape = schema.shape;
|
|
1851
|
+
const renderField = (fieldName, fieldSchema) => {
|
|
1852
|
+
const fieldType = resolveFieldType(fieldSchema);
|
|
1853
|
+
const label = fieldNameToLabel(fieldName);
|
|
1854
|
+
const isRequired = !fieldSchema.isOptional();
|
|
1855
|
+
const fieldError = errors[fieldName];
|
|
1856
|
+
const errorMessage = fieldError?.message;
|
|
1857
|
+
switch (fieldType) {
|
|
1858
|
+
case "string":
|
|
1859
|
+
return /* @__PURE__ */ jsx24(
|
|
1860
|
+
StringInput,
|
|
1861
|
+
{
|
|
1862
|
+
label,
|
|
1863
|
+
required: isRequired,
|
|
1864
|
+
error: errorMessage,
|
|
1865
|
+
...register(fieldName)
|
|
1866
|
+
},
|
|
1867
|
+
fieldName
|
|
1868
|
+
);
|
|
1869
|
+
case "text":
|
|
1870
|
+
return /* @__PURE__ */ jsx24(
|
|
1871
|
+
TextInput,
|
|
1872
|
+
{
|
|
1873
|
+
label,
|
|
1874
|
+
required: isRequired,
|
|
1875
|
+
error: errorMessage,
|
|
1876
|
+
...register(fieldName)
|
|
1877
|
+
},
|
|
1878
|
+
fieldName
|
|
1879
|
+
);
|
|
1880
|
+
case "richtext":
|
|
1881
|
+
return /* @__PURE__ */ jsx24(
|
|
1882
|
+
Controller,
|
|
1883
|
+
{
|
|
1884
|
+
name: fieldName,
|
|
1885
|
+
control,
|
|
1886
|
+
render: ({ field }) => /* @__PURE__ */ jsx24(
|
|
1887
|
+
RichTextEditor,
|
|
1888
|
+
{
|
|
1889
|
+
label,
|
|
1890
|
+
required: isRequired,
|
|
1891
|
+
error: errorMessage,
|
|
1892
|
+
value: field.value,
|
|
1893
|
+
onChange: field.onChange,
|
|
1894
|
+
name: field.name
|
|
1895
|
+
}
|
|
1896
|
+
)
|
|
1897
|
+
},
|
|
1898
|
+
fieldName
|
|
1899
|
+
);
|
|
1900
|
+
case "image":
|
|
1901
|
+
return /* @__PURE__ */ jsx24(
|
|
1902
|
+
Controller,
|
|
1903
|
+
{
|
|
1904
|
+
name: fieldName,
|
|
1905
|
+
control,
|
|
1906
|
+
render: ({ field }) => /* @__PURE__ */ jsx24(
|
|
1907
|
+
ImagePicker,
|
|
1908
|
+
{
|
|
1909
|
+
label,
|
|
1910
|
+
required: isRequired,
|
|
1911
|
+
error: errorMessage,
|
|
1912
|
+
value: field.value,
|
|
1913
|
+
onChange: field.onChange,
|
|
1914
|
+
name: field.name
|
|
1915
|
+
}
|
|
1916
|
+
)
|
|
1917
|
+
},
|
|
1918
|
+
fieldName
|
|
1919
|
+
);
|
|
1920
|
+
case "array":
|
|
1921
|
+
return /* @__PURE__ */ jsx24(
|
|
1922
|
+
Controller,
|
|
1923
|
+
{
|
|
1924
|
+
name: fieldName,
|
|
1925
|
+
control,
|
|
1926
|
+
render: ({ field }) => /* @__PURE__ */ jsx24(
|
|
1927
|
+
ArrayField,
|
|
1928
|
+
{
|
|
1929
|
+
label,
|
|
1930
|
+
required: isRequired,
|
|
1931
|
+
error: errorMessage,
|
|
1932
|
+
value: field.value ?? [],
|
|
1933
|
+
onChange: field.onChange,
|
|
1934
|
+
name: field.name,
|
|
1935
|
+
createDefaultItem: () => "",
|
|
1936
|
+
renderItem: (item, index, onItemChange) => /* @__PURE__ */ jsx24(
|
|
1937
|
+
Input,
|
|
1938
|
+
{
|
|
1939
|
+
value: item,
|
|
1940
|
+
onChange: (e) => onItemChange(e.target.value),
|
|
1941
|
+
"data-testid": `${fieldName}-item-${index}`
|
|
1942
|
+
}
|
|
1943
|
+
)
|
|
1944
|
+
}
|
|
1945
|
+
)
|
|
1946
|
+
},
|
|
1947
|
+
fieldName
|
|
1948
|
+
);
|
|
1949
|
+
case "object": {
|
|
1950
|
+
const innerSchema = unwrapSchema(fieldSchema);
|
|
1951
|
+
const innerShape = "shape" in innerSchema ? innerSchema.shape : null;
|
|
1952
|
+
return /* @__PURE__ */ jsx24(ObjectField, { label, required: isRequired, error: errorMessage, children: innerShape ? Object.entries(innerShape).map(([subName, _subSchema]) => {
|
|
1953
|
+
const subFieldName = `${fieldName}.${subName}`;
|
|
1954
|
+
const subLabel = fieldNameToLabel(subName);
|
|
1955
|
+
const subError = errors[fieldName];
|
|
1956
|
+
const subErrorMessage = subError?.[subName]?.message;
|
|
1957
|
+
return /* @__PURE__ */ jsx24(
|
|
1958
|
+
StringInput,
|
|
1959
|
+
{
|
|
1960
|
+
label: subLabel,
|
|
1961
|
+
error: subErrorMessage,
|
|
1962
|
+
...register(subFieldName)
|
|
1963
|
+
},
|
|
1964
|
+
subFieldName
|
|
1965
|
+
);
|
|
1966
|
+
}) : null }, fieldName);
|
|
1967
|
+
}
|
|
1968
|
+
default:
|
|
1969
|
+
return /* @__PURE__ */ jsx24(
|
|
1970
|
+
StringInput,
|
|
1971
|
+
{
|
|
1972
|
+
label,
|
|
1973
|
+
required: isRequired,
|
|
1974
|
+
error: errorMessage,
|
|
1975
|
+
...register(fieldName)
|
|
1976
|
+
},
|
|
1977
|
+
fieldName
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1980
|
+
};
|
|
1981
|
+
return /* @__PURE__ */ jsxs17(
|
|
1982
|
+
"form",
|
|
1983
|
+
{
|
|
1984
|
+
onSubmit: handleSubmit(onSubmit),
|
|
1985
|
+
className: cn("space-y-4", className),
|
|
1986
|
+
"data-testid": "form-generator",
|
|
1987
|
+
children: [
|
|
1988
|
+
Object.entries(shape).map(([fieldName, fieldSchema]) => renderField(fieldName, fieldSchema)),
|
|
1989
|
+
!onChange && /* @__PURE__ */ jsx24(Button, { type: "submit", "data-testid": "form-submit", children: submitLabel })
|
|
1990
|
+
]
|
|
1991
|
+
}
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
FormGenerator.displayName = "FormGenerator";
|
|
1995
|
+
|
|
1996
|
+
// src/components/editors/section-editor.tsx
|
|
1997
|
+
import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
1998
|
+
function SectionEditor({
|
|
1999
|
+
sectionType,
|
|
2000
|
+
data,
|
|
2001
|
+
onChange,
|
|
2002
|
+
submitLabel = "Save Section",
|
|
2003
|
+
className
|
|
2004
|
+
}) {
|
|
2005
|
+
const { registry } = useAdmin();
|
|
2006
|
+
const section = registry.getSection(sectionType);
|
|
2007
|
+
if (!section) {
|
|
2008
|
+
return /* @__PURE__ */ jsx25(
|
|
2009
|
+
"div",
|
|
2010
|
+
{
|
|
2011
|
+
className: cn("rounded-md border border-destructive p-4", className),
|
|
2012
|
+
"data-testid": "section-editor-error",
|
|
2013
|
+
children: /* @__PURE__ */ jsxs18("p", { className: "text-sm text-destructive", children: [
|
|
2014
|
+
"Unknown section type: ",
|
|
2015
|
+
/* @__PURE__ */ jsx25("strong", { children: sectionType })
|
|
2016
|
+
] })
|
|
2017
|
+
}
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
return /* @__PURE__ */ jsxs18("div", { className: cn("space-y-4", className), "data-testid": "section-editor", children: [
|
|
2021
|
+
/* @__PURE__ */ jsx25("h3", { className: "text-lg font-semibold capitalize", children: section.name }),
|
|
2022
|
+
/* @__PURE__ */ jsx25(
|
|
2023
|
+
FormGenerator,
|
|
2024
|
+
{
|
|
2025
|
+
schema: section.schema,
|
|
2026
|
+
onSubmit: onChange,
|
|
2027
|
+
onChange,
|
|
2028
|
+
defaultValues: data,
|
|
2029
|
+
submitLabel
|
|
2030
|
+
}
|
|
2031
|
+
)
|
|
2032
|
+
] });
|
|
2033
|
+
}
|
|
2034
|
+
SectionEditor.displayName = "SectionEditor";
|
|
2035
|
+
|
|
2036
|
+
// src/components/editors/page-editor.tsx
|
|
2037
|
+
import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
2038
|
+
function PageEditor({
|
|
2039
|
+
sections: initialSections,
|
|
2040
|
+
allowedSections,
|
|
2041
|
+
onSave,
|
|
2042
|
+
className
|
|
2043
|
+
}) {
|
|
2044
|
+
const { registry } = useAdmin();
|
|
2045
|
+
const [sections, setSections] = React20.useState(initialSections);
|
|
2046
|
+
const [selectedSectionType, setSelectedSectionType] = React20.useState(
|
|
2047
|
+
allowedSections[0] ?? ""
|
|
2048
|
+
);
|
|
2049
|
+
const handleAddSection = () => {
|
|
2050
|
+
if (!selectedSectionType) return;
|
|
2051
|
+
const newSection = {
|
|
2052
|
+
type: selectedSectionType,
|
|
2053
|
+
data: {}
|
|
2054
|
+
};
|
|
2055
|
+
setSections([...sections, newSection]);
|
|
2056
|
+
};
|
|
2057
|
+
const handleRemoveSection = (index) => {
|
|
2058
|
+
const newSections = [...sections];
|
|
2059
|
+
newSections.splice(index, 1);
|
|
2060
|
+
setSections(newSections);
|
|
2061
|
+
};
|
|
2062
|
+
const handleMoveUp = (index) => {
|
|
2063
|
+
if (index === 0) return;
|
|
2064
|
+
const newSections = [...sections];
|
|
2065
|
+
const temp = newSections[index];
|
|
2066
|
+
newSections[index] = newSections[index - 1];
|
|
2067
|
+
newSections[index - 1] = temp;
|
|
2068
|
+
setSections(newSections);
|
|
2069
|
+
};
|
|
2070
|
+
const handleMoveDown = (index) => {
|
|
2071
|
+
if (index === sections.length - 1) return;
|
|
2072
|
+
const newSections = [...sections];
|
|
2073
|
+
const temp = newSections[index];
|
|
2074
|
+
newSections[index] = newSections[index + 1];
|
|
2075
|
+
newSections[index + 1] = temp;
|
|
2076
|
+
setSections(newSections);
|
|
2077
|
+
};
|
|
2078
|
+
const handleSectionChange = (index, data) => {
|
|
2079
|
+
const newSections = [...sections];
|
|
2080
|
+
const current = newSections[index];
|
|
2081
|
+
if (current) {
|
|
2082
|
+
newSections[index] = { ...current, data };
|
|
2083
|
+
}
|
|
2084
|
+
setSections(newSections);
|
|
2085
|
+
};
|
|
2086
|
+
const handleSave = () => {
|
|
2087
|
+
onSave(sections);
|
|
2088
|
+
};
|
|
2089
|
+
return /* @__PURE__ */ jsxs19("div", { className: cn("space-y-6", className), "data-testid": "page-editor", children: [
|
|
2090
|
+
sections.length === 0 ? /* @__PURE__ */ jsx26("p", { className: "text-sm text-muted-foreground text-center py-8", children: "No sections yet. Add a section to get started." }) : /* @__PURE__ */ jsx26("div", { className: "space-y-4", children: sections.map((section, index) => {
|
|
2091
|
+
const sectionDef = registry.getSection(section.type);
|
|
2092
|
+
const sectionLabel = sectionDef?.name ?? section.type;
|
|
2093
|
+
return /* @__PURE__ */ jsxs19(
|
|
2094
|
+
"div",
|
|
2095
|
+
{
|
|
2096
|
+
className: "rounded-md border border-input bg-background p-4",
|
|
2097
|
+
"data-testid": `page-section-${index}`,
|
|
2098
|
+
children: [
|
|
2099
|
+
/* @__PURE__ */ jsxs19("div", { className: "flex items-center justify-between mb-4", children: [
|
|
2100
|
+
/* @__PURE__ */ jsx26("h3", { className: "text-sm font-semibold capitalize", children: sectionLabel }),
|
|
2101
|
+
/* @__PURE__ */ jsxs19("div", { className: "flex gap-1", children: [
|
|
2102
|
+
/* @__PURE__ */ jsx26(
|
|
2103
|
+
Button,
|
|
2104
|
+
{
|
|
2105
|
+
type: "button",
|
|
2106
|
+
variant: "ghost",
|
|
2107
|
+
size: "icon",
|
|
2108
|
+
onClick: () => handleMoveUp(index),
|
|
2109
|
+
disabled: index === 0,
|
|
2110
|
+
title: "Move up",
|
|
2111
|
+
"data-testid": `section-move-up-${index}`,
|
|
2112
|
+
children: "\u2191"
|
|
2113
|
+
}
|
|
2114
|
+
),
|
|
2115
|
+
/* @__PURE__ */ jsx26(
|
|
2116
|
+
Button,
|
|
2117
|
+
{
|
|
2118
|
+
type: "button",
|
|
2119
|
+
variant: "ghost",
|
|
2120
|
+
size: "icon",
|
|
2121
|
+
onClick: () => handleMoveDown(index),
|
|
2122
|
+
disabled: index === sections.length - 1,
|
|
2123
|
+
title: "Move down",
|
|
2124
|
+
"data-testid": `section-move-down-${index}`,
|
|
2125
|
+
children: "\u2193"
|
|
2126
|
+
}
|
|
2127
|
+
),
|
|
2128
|
+
/* @__PURE__ */ jsx26(
|
|
2129
|
+
Button,
|
|
2130
|
+
{
|
|
2131
|
+
type: "button",
|
|
2132
|
+
variant: "ghost",
|
|
2133
|
+
size: "icon",
|
|
2134
|
+
onClick: () => handleRemoveSection(index),
|
|
2135
|
+
title: "Remove section",
|
|
2136
|
+
"data-testid": `section-remove-${index}`,
|
|
2137
|
+
children: "\u2715"
|
|
2138
|
+
}
|
|
2139
|
+
)
|
|
2140
|
+
] })
|
|
2141
|
+
] }),
|
|
2142
|
+
/* @__PURE__ */ jsx26(
|
|
2143
|
+
SectionEditor,
|
|
2144
|
+
{
|
|
2145
|
+
sectionType: section.type,
|
|
2146
|
+
data: section.data,
|
|
2147
|
+
onChange: (data) => handleSectionChange(index, data),
|
|
2148
|
+
submitLabel: "Update Section"
|
|
2149
|
+
}
|
|
2150
|
+
)
|
|
2151
|
+
]
|
|
2152
|
+
},
|
|
2153
|
+
`${section.type}-${index}`
|
|
2154
|
+
);
|
|
2155
|
+
}) }),
|
|
2156
|
+
/* @__PURE__ */ jsxs19("div", { className: "flex items-center gap-2 border-t border-input pt-4", children: [
|
|
2157
|
+
/* @__PURE__ */ jsx26(
|
|
2158
|
+
"select",
|
|
2159
|
+
{
|
|
2160
|
+
value: selectedSectionType,
|
|
2161
|
+
onChange: (e) => setSelectedSectionType(e.target.value),
|
|
2162
|
+
className: "flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm",
|
|
2163
|
+
"data-testid": "section-type-select",
|
|
2164
|
+
children: allowedSections.map((type) => {
|
|
2165
|
+
const sectionDef = registry.getSection(type);
|
|
2166
|
+
return /* @__PURE__ */ jsx26("option", { value: type, children: sectionDef?.name ?? type }, type);
|
|
2167
|
+
})
|
|
2168
|
+
}
|
|
2169
|
+
),
|
|
2170
|
+
/* @__PURE__ */ jsx26(
|
|
2171
|
+
Button,
|
|
2172
|
+
{
|
|
2173
|
+
type: "button",
|
|
2174
|
+
variant: "outline",
|
|
2175
|
+
onClick: handleAddSection,
|
|
2176
|
+
"data-testid": "add-section",
|
|
2177
|
+
children: "Add Section"
|
|
2178
|
+
}
|
|
2179
|
+
)
|
|
2180
|
+
] }),
|
|
2181
|
+
/* @__PURE__ */ jsx26("div", { className: "border-t border-input pt-4", children: /* @__PURE__ */ jsx26(Button, { type: "button", onClick: handleSave, "data-testid": "save-page", children: "Save Page" }) })
|
|
2182
|
+
] });
|
|
2183
|
+
}
|
|
2184
|
+
PageEditor.displayName = "PageEditor";
|
|
2185
|
+
|
|
2186
|
+
// src/components/layout/admin-layout.tsx
|
|
2187
|
+
import * as React21 from "react";
|
|
2188
|
+
import { jsx as jsx27, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
2189
|
+
var DEFAULT_NAV_ITEMS = [
|
|
2190
|
+
{ label: "Pages", path: "/pages" },
|
|
2191
|
+
{ label: "Navigation", path: "/navigation" },
|
|
2192
|
+
{ label: "Media", path: "/media" }
|
|
2193
|
+
];
|
|
2194
|
+
function AdminLayout({
|
|
2195
|
+
children,
|
|
2196
|
+
title = "StructCMS",
|
|
2197
|
+
navItems = DEFAULT_NAV_ITEMS,
|
|
2198
|
+
activePath,
|
|
2199
|
+
onNavigate,
|
|
2200
|
+
className
|
|
2201
|
+
}) {
|
|
2202
|
+
const [sidebarOpen, setSidebarOpen] = React21.useState(false);
|
|
2203
|
+
return /* @__PURE__ */ jsxs20("div", { className: cn("flex h-screen bg-background", className), "data-testid": "admin-layout", children: [
|
|
2204
|
+
sidebarOpen && /* @__PURE__ */ jsx27(
|
|
2205
|
+
"div",
|
|
2206
|
+
{
|
|
2207
|
+
className: "fixed inset-0 z-40 bg-black/50 md:hidden",
|
|
2208
|
+
onClick: () => setSidebarOpen(false),
|
|
2209
|
+
onKeyDown: (e) => {
|
|
2210
|
+
if (e.key === "Enter" || e.key === "Escape") {
|
|
2211
|
+
setSidebarOpen(false);
|
|
2212
|
+
}
|
|
2213
|
+
},
|
|
2214
|
+
role: "button",
|
|
2215
|
+
tabIndex: 0,
|
|
2216
|
+
"data-testid": "sidebar-overlay"
|
|
2217
|
+
}
|
|
2218
|
+
),
|
|
2219
|
+
/* @__PURE__ */ jsxs20(
|
|
2220
|
+
"aside",
|
|
2221
|
+
{
|
|
2222
|
+
className: cn(
|
|
2223
|
+
"fixed inset-y-0 left-0 z-50 w-64 bg-card border-r border-input transform transition-transform duration-200 md:relative md:translate-x-0",
|
|
2224
|
+
sidebarOpen ? "translate-x-0" : "-translate-x-full"
|
|
2225
|
+
),
|
|
2226
|
+
"data-testid": "sidebar",
|
|
2227
|
+
children: [
|
|
2228
|
+
/* @__PURE__ */ jsx27("div", { className: "flex items-center h-14 px-4 border-b border-input", children: /* @__PURE__ */ jsx27("h1", { className: "text-lg font-bold", "data-testid": "sidebar-title", children: title }) }),
|
|
2229
|
+
/* @__PURE__ */ jsx27("nav", { className: "p-2 space-y-1", "data-testid": "sidebar-nav", children: navItems.map((item) => /* @__PURE__ */ jsx27(
|
|
2230
|
+
"button",
|
|
2231
|
+
{
|
|
2232
|
+
type: "button",
|
|
2233
|
+
className: cn(
|
|
2234
|
+
"w-full text-left px-3 py-2 rounded-md text-sm transition-colors",
|
|
2235
|
+
activePath === item.path ? "bg-primary text-primary-foreground" : "hover:bg-muted"
|
|
2236
|
+
),
|
|
2237
|
+
onClick: () => {
|
|
2238
|
+
onNavigate(item.path);
|
|
2239
|
+
setSidebarOpen(false);
|
|
2240
|
+
},
|
|
2241
|
+
"data-testid": `nav-link-${item.path}`,
|
|
2242
|
+
children: item.label
|
|
2243
|
+
},
|
|
2244
|
+
item.path
|
|
2245
|
+
)) })
|
|
2246
|
+
]
|
|
2247
|
+
}
|
|
2248
|
+
),
|
|
2249
|
+
/* @__PURE__ */ jsxs20("div", { className: "flex-1 flex flex-col min-w-0", children: [
|
|
2250
|
+
/* @__PURE__ */ jsxs20(
|
|
2251
|
+
"header",
|
|
2252
|
+
{
|
|
2253
|
+
className: "flex items-center h-14 px-4 border-b border-input bg-card",
|
|
2254
|
+
"data-testid": "header",
|
|
2255
|
+
children: [
|
|
2256
|
+
/* @__PURE__ */ jsx27(
|
|
2257
|
+
Button,
|
|
2258
|
+
{
|
|
2259
|
+
type: "button",
|
|
2260
|
+
variant: "ghost",
|
|
2261
|
+
size: "icon",
|
|
2262
|
+
className: "md:hidden mr-2",
|
|
2263
|
+
onClick: () => setSidebarOpen(!sidebarOpen),
|
|
2264
|
+
"data-testid": "sidebar-toggle",
|
|
2265
|
+
children: "\u2630"
|
|
2266
|
+
}
|
|
2267
|
+
),
|
|
2268
|
+
/* @__PURE__ */ jsx27("h2", { className: "text-lg font-semibold", "data-testid": "header-title", children: title })
|
|
2269
|
+
]
|
|
2270
|
+
}
|
|
2271
|
+
),
|
|
2272
|
+
/* @__PURE__ */ jsx27("main", { className: "flex-1 overflow-auto p-6", "data-testid": "main-content", children })
|
|
2273
|
+
] })
|
|
2274
|
+
] });
|
|
2275
|
+
}
|
|
2276
|
+
AdminLayout.displayName = "AdminLayout";
|
|
2277
|
+
|
|
2278
|
+
// src/components/app/struct-cms-admin-app.tsx
|
|
2279
|
+
import { jsx as jsx28, jsxs as jsxs21 } from "react/jsx-runtime";
|
|
2280
|
+
function useNavigationData(currentView) {
|
|
2281
|
+
const apiClient = useApiClient();
|
|
2282
|
+
const [navigationData, setNavigationData] = useState11(null);
|
|
2283
|
+
const [navigationLoading, setNavigationLoading] = useState11(false);
|
|
2284
|
+
const [navigationError, setNavigationError] = useState11(null);
|
|
2285
|
+
useEffect8(() => {
|
|
2286
|
+
if (currentView.type !== "navigation") {
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
const fetchNavigation = async () => {
|
|
2290
|
+
setNavigationLoading(true);
|
|
2291
|
+
setNavigationError(null);
|
|
2292
|
+
try {
|
|
2293
|
+
const response = await apiClient.get("/navigation/main");
|
|
2294
|
+
if (response.error) {
|
|
2295
|
+
setNavigationError(response.error.message);
|
|
2296
|
+
} else if (response.data) {
|
|
2297
|
+
setNavigationData(response.data);
|
|
2298
|
+
}
|
|
2299
|
+
} catch (err) {
|
|
2300
|
+
setNavigationError("Failed to load navigation");
|
|
2301
|
+
console.error(err);
|
|
2302
|
+
} finally {
|
|
2303
|
+
setNavigationLoading(false);
|
|
2304
|
+
}
|
|
2305
|
+
};
|
|
2306
|
+
fetchNavigation();
|
|
2307
|
+
}, [currentView.type, apiClient]);
|
|
2308
|
+
return { navigationData, navigationLoading, navigationError, setNavigationData };
|
|
2309
|
+
}
|
|
2310
|
+
function usePageData(currentView) {
|
|
2311
|
+
const apiClient = useApiClient();
|
|
2312
|
+
const [pageData, setPageData] = useState11(null);
|
|
2313
|
+
const [pageLoading, setPageLoading] = useState11(false);
|
|
2314
|
+
const [pageError, setPageError] = useState11(null);
|
|
2315
|
+
useEffect8(() => {
|
|
2316
|
+
if (currentView.type !== "page-editor") {
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
const pageId = currentView.pageId;
|
|
2320
|
+
if (!pageId) {
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
const fetchPage = async () => {
|
|
2324
|
+
setPageLoading(true);
|
|
2325
|
+
setPageError(null);
|
|
2326
|
+
try {
|
|
2327
|
+
const response = await apiClient.get(`/pages/id/${pageId}`);
|
|
2328
|
+
if (response.error) {
|
|
2329
|
+
setPageError(response.error.message);
|
|
2330
|
+
} else if (response.data) {
|
|
2331
|
+
setPageData(response.data);
|
|
2332
|
+
}
|
|
2333
|
+
} catch (err) {
|
|
2334
|
+
setPageError("Failed to load page");
|
|
2335
|
+
console.error(err);
|
|
2336
|
+
} finally {
|
|
2337
|
+
setPageLoading(false);
|
|
2338
|
+
}
|
|
2339
|
+
};
|
|
2340
|
+
fetchPage();
|
|
2341
|
+
}, [currentView, apiClient]);
|
|
2342
|
+
return { pageData, pageLoading, pageError };
|
|
2343
|
+
}
|
|
2344
|
+
function renderPageEditorView(currentView, pageData, pageLoading, pageError, registry, onSave) {
|
|
2345
|
+
if (currentView.type !== "page-editor") return null;
|
|
2346
|
+
if (currentView.pageId && pageLoading) {
|
|
2347
|
+
return /* @__PURE__ */ jsxs21("div", { className: "space-y-4", children: [
|
|
2348
|
+
/* @__PURE__ */ jsx28(Skeleton, { className: "h-8 w-48" }),
|
|
2349
|
+
/* @__PURE__ */ jsx28(Skeleton, { className: "h-64 w-full" })
|
|
2350
|
+
] });
|
|
2351
|
+
}
|
|
2352
|
+
if (currentView.pageId && pageError) {
|
|
2353
|
+
return /* @__PURE__ */ jsxs21("div", { className: "text-red-600", children: [
|
|
2354
|
+
"Error: ",
|
|
2355
|
+
pageError
|
|
2356
|
+
] });
|
|
2357
|
+
}
|
|
2358
|
+
if (currentView.pageId && !pageData) {
|
|
2359
|
+
return /* @__PURE__ */ jsx28("div", { className: "text-gray-600", children: "Page not found" });
|
|
2360
|
+
}
|
|
2361
|
+
const sections = pageData?.sections ?? [];
|
|
2362
|
+
const allowedSections = pageData ? registry.getPageType(pageData.pageType)?.allowedSections ?? [] : registry.getAllSections().map((s) => s.name);
|
|
2363
|
+
return /* @__PURE__ */ jsx28(PageEditor, { sections, allowedSections, onSave });
|
|
2364
|
+
}
|
|
2365
|
+
function renderNavigationView(navigationData, navigationLoading, navigationError, onSave) {
|
|
2366
|
+
if (navigationLoading) {
|
|
2367
|
+
return /* @__PURE__ */ jsxs21("div", { className: "space-y-4", children: [
|
|
2368
|
+
/* @__PURE__ */ jsx28(Skeleton, { className: "h-8 w-48" }),
|
|
2369
|
+
/* @__PURE__ */ jsx28(Skeleton, { className: "h-64 w-full" })
|
|
2370
|
+
] });
|
|
2371
|
+
}
|
|
2372
|
+
if (navigationError) {
|
|
2373
|
+
return /* @__PURE__ */ jsxs21("div", { className: "text-red-600", children: [
|
|
2374
|
+
"Error: ",
|
|
2375
|
+
navigationError
|
|
2376
|
+
] });
|
|
2377
|
+
}
|
|
2378
|
+
if (!navigationData) {
|
|
2379
|
+
return /* @__PURE__ */ jsx28("div", { className: "text-gray-600", children: "No navigation found. Create one via the seed endpoint." });
|
|
2380
|
+
}
|
|
2381
|
+
return /* @__PURE__ */ jsx28(NavigationEditor, { items: navigationData.items, onSave });
|
|
2382
|
+
}
|
|
2383
|
+
function ViewRenderer({
|
|
2384
|
+
currentView,
|
|
2385
|
+
registry,
|
|
2386
|
+
customRenderView,
|
|
2387
|
+
onNavigate,
|
|
2388
|
+
onSelectPage,
|
|
2389
|
+
onCreatePage,
|
|
2390
|
+
onUploadMedia
|
|
2391
|
+
}) {
|
|
2392
|
+
const apiClient = useApiClient();
|
|
2393
|
+
const { navigationData, navigationLoading, navigationError, setNavigationData } = useNavigationData(currentView);
|
|
2394
|
+
const { pageData, pageLoading, pageError } = usePageData(currentView);
|
|
2395
|
+
const handleSaveNavigation = async (items) => {
|
|
2396
|
+
if (!navigationData) return;
|
|
2397
|
+
try {
|
|
2398
|
+
const response = await apiClient.put(`/navigation/id/${navigationData.id}`, {
|
|
2399
|
+
items
|
|
2400
|
+
});
|
|
2401
|
+
if (response.error) {
|
|
2402
|
+
console.error("Failed to update navigation:", response.error.message);
|
|
2403
|
+
} else if (response.data) {
|
|
2404
|
+
setNavigationData(response.data);
|
|
2405
|
+
}
|
|
2406
|
+
} catch (err) {
|
|
2407
|
+
console.error("Failed to update navigation:", err);
|
|
2408
|
+
}
|
|
2409
|
+
};
|
|
2410
|
+
const handleSavePage = async (updatedSections) => {
|
|
2411
|
+
if (!pageData) return;
|
|
2412
|
+
try {
|
|
2413
|
+
const response = await apiClient.put(`/pages/id/${pageData.id}`, {
|
|
2414
|
+
title: pageData.title,
|
|
2415
|
+
sections: updatedSections
|
|
2416
|
+
});
|
|
2417
|
+
if (response.error) {
|
|
2418
|
+
console.error("Failed to update page:", response.error.message);
|
|
2419
|
+
} else {
|
|
2420
|
+
onNavigate({ type: "pages" });
|
|
2421
|
+
}
|
|
2422
|
+
} catch (err) {
|
|
2423
|
+
console.error("Failed to update page:", err);
|
|
2424
|
+
}
|
|
2425
|
+
};
|
|
2426
|
+
if (customRenderView) {
|
|
2427
|
+
const customView = customRenderView(currentView);
|
|
2428
|
+
if (customView !== null) {
|
|
2429
|
+
return customView;
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
switch (currentView.type) {
|
|
2433
|
+
case "dashboard":
|
|
2434
|
+
return /* @__PURE__ */ jsx28(
|
|
2435
|
+
DashboardPage,
|
|
2436
|
+
{
|
|
2437
|
+
onSelectPage,
|
|
2438
|
+
onCreatePage,
|
|
2439
|
+
onUploadMedia
|
|
2440
|
+
}
|
|
2441
|
+
);
|
|
2442
|
+
case "pages":
|
|
2443
|
+
return /* @__PURE__ */ jsx28(PageList, { onSelectPage, onCreatePage });
|
|
2444
|
+
case "page-editor":
|
|
2445
|
+
return renderPageEditorView(
|
|
2446
|
+
currentView,
|
|
2447
|
+
pageData,
|
|
2448
|
+
pageLoading,
|
|
2449
|
+
pageError,
|
|
2450
|
+
registry,
|
|
2451
|
+
handleSavePage
|
|
2452
|
+
);
|
|
2453
|
+
case "media":
|
|
2454
|
+
return /* @__PURE__ */ jsx28(MediaBrowser, { onSelect: () => {
|
|
2455
|
+
} });
|
|
2456
|
+
case "navigation":
|
|
2457
|
+
return renderNavigationView(
|
|
2458
|
+
navigationData,
|
|
2459
|
+
navigationLoading,
|
|
2460
|
+
navigationError,
|
|
2461
|
+
handleSaveNavigation
|
|
2462
|
+
);
|
|
2463
|
+
case "custom":
|
|
2464
|
+
return /* @__PURE__ */ jsxs21("div", { "data-testid": "custom-view", children: [
|
|
2465
|
+
"Custom view for path: ",
|
|
2466
|
+
currentView.path
|
|
2467
|
+
] });
|
|
2468
|
+
default:
|
|
2469
|
+
return null;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
function StructCMSAdminApp({
|
|
2473
|
+
registry,
|
|
2474
|
+
apiBaseUrl = "/api/cms",
|
|
2475
|
+
className,
|
|
2476
|
+
customNavItems = [],
|
|
2477
|
+
renderView: customRenderView,
|
|
2478
|
+
enableAuth = false,
|
|
2479
|
+
onAuthStateChange
|
|
2480
|
+
}) {
|
|
2481
|
+
const [currentView, setCurrentView] = useState11({ type: "dashboard" });
|
|
2482
|
+
const handleNavigate = (path) => {
|
|
2483
|
+
if (path === "/") {
|
|
2484
|
+
setCurrentView({ type: "dashboard" });
|
|
2485
|
+
} else if (path === "/pages") {
|
|
2486
|
+
setCurrentView({ type: "pages" });
|
|
2487
|
+
} else if (path === "/media") {
|
|
2488
|
+
setCurrentView({ type: "media" });
|
|
2489
|
+
} else if (path === "/navigation") {
|
|
2490
|
+
setCurrentView({ type: "navigation" });
|
|
2491
|
+
} else {
|
|
2492
|
+
setCurrentView({ type: "custom", path });
|
|
2493
|
+
}
|
|
2494
|
+
};
|
|
2495
|
+
const handleNavigateToView = (view) => {
|
|
2496
|
+
setCurrentView(view);
|
|
2497
|
+
};
|
|
2498
|
+
const handleSelectPage = (page) => {
|
|
2499
|
+
setCurrentView({ type: "page-editor", pageId: page.id });
|
|
2500
|
+
};
|
|
2501
|
+
const handleCreatePage = () => {
|
|
2502
|
+
setCurrentView({ type: "page-editor" });
|
|
2503
|
+
};
|
|
2504
|
+
const handleUploadMedia = () => {
|
|
2505
|
+
setCurrentView({ type: "media" });
|
|
2506
|
+
};
|
|
2507
|
+
const defaultNavItems = [
|
|
2508
|
+
{ label: "Dashboard", path: "/" },
|
|
2509
|
+
{ label: "Pages", path: "/pages" },
|
|
2510
|
+
{ label: "Navigation", path: "/navigation" },
|
|
2511
|
+
{ label: "Media", path: "/media" }
|
|
2512
|
+
];
|
|
2513
|
+
const navItems = [...defaultNavItems, ...customNavItems];
|
|
2514
|
+
const appContent = /* @__PURE__ */ jsx28(AdminProvider, { registry, apiBaseUrl, children: /* @__PURE__ */ jsx28(AdminLayout, { className, navItems, onNavigate: handleNavigate, children: /* @__PURE__ */ jsx28(
|
|
2515
|
+
ViewRenderer,
|
|
2516
|
+
{
|
|
2517
|
+
currentView,
|
|
2518
|
+
registry,
|
|
2519
|
+
customRenderView,
|
|
2520
|
+
onNavigate: handleNavigateToView,
|
|
2521
|
+
onSelectPage: handleSelectPage,
|
|
2522
|
+
onCreatePage: handleCreatePage,
|
|
2523
|
+
onUploadMedia: handleUploadMedia
|
|
2524
|
+
}
|
|
2525
|
+
) }) });
|
|
2526
|
+
if (enableAuth) {
|
|
2527
|
+
return /* @__PURE__ */ jsx28(AuthProvider, { apiBaseUrl, onAuthStateChange, children: appContent });
|
|
2528
|
+
}
|
|
2529
|
+
return appContent;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// src/components/auth/login-form.tsx
|
|
2533
|
+
import { useState as useState12 } from "react";
|
|
2534
|
+
import { jsx as jsx29, jsxs as jsxs22 } from "react/jsx-runtime";
|
|
2535
|
+
function LoginForm({ onSuccess, onError }) {
|
|
2536
|
+
const { signIn } = useAuth();
|
|
2537
|
+
const [email, setEmail] = useState12("");
|
|
2538
|
+
const [password, setPassword] = useState12("");
|
|
2539
|
+
const [error, setError] = useState12(null);
|
|
2540
|
+
const [isSubmitting, setIsSubmitting] = useState12(false);
|
|
2541
|
+
const handleSubmit = async (e) => {
|
|
2542
|
+
e.preventDefault();
|
|
2543
|
+
setError(null);
|
|
2544
|
+
setIsSubmitting(true);
|
|
2545
|
+
try {
|
|
2546
|
+
await signIn(email, password);
|
|
2547
|
+
onSuccess?.();
|
|
2548
|
+
} catch (err) {
|
|
2549
|
+
const errorMessage = err instanceof Error ? err.message : "Sign in failed";
|
|
2550
|
+
setError(errorMessage);
|
|
2551
|
+
onError?.(err instanceof Error ? err : new Error(errorMessage));
|
|
2552
|
+
} finally {
|
|
2553
|
+
setIsSubmitting(false);
|
|
2554
|
+
}
|
|
2555
|
+
};
|
|
2556
|
+
return /* @__PURE__ */ jsxs22("form", { onSubmit: handleSubmit, className: "space-y-4", children: [
|
|
2557
|
+
/* @__PURE__ */ jsxs22("div", { className: "space-y-2", children: [
|
|
2558
|
+
/* @__PURE__ */ jsx29(Label, { htmlFor: "email", children: "Email" }),
|
|
2559
|
+
/* @__PURE__ */ jsx29(
|
|
2560
|
+
Input,
|
|
2561
|
+
{
|
|
2562
|
+
id: "email",
|
|
2563
|
+
type: "email",
|
|
2564
|
+
value: email,
|
|
2565
|
+
onChange: (e) => setEmail(e.target.value),
|
|
2566
|
+
placeholder: "admin@example.com",
|
|
2567
|
+
required: true,
|
|
2568
|
+
disabled: isSubmitting
|
|
2569
|
+
}
|
|
2570
|
+
)
|
|
2571
|
+
] }),
|
|
2572
|
+
/* @__PURE__ */ jsxs22("div", { className: "space-y-2", children: [
|
|
2573
|
+
/* @__PURE__ */ jsx29(Label, { htmlFor: "password", children: "Password" }),
|
|
2574
|
+
/* @__PURE__ */ jsx29(
|
|
2575
|
+
Input,
|
|
2576
|
+
{
|
|
2577
|
+
id: "password",
|
|
2578
|
+
type: "password",
|
|
2579
|
+
value: password,
|
|
2580
|
+
onChange: (e) => setPassword(e.target.value),
|
|
2581
|
+
placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
|
|
2582
|
+
required: true,
|
|
2583
|
+
disabled: isSubmitting
|
|
2584
|
+
}
|
|
2585
|
+
)
|
|
2586
|
+
] }),
|
|
2587
|
+
error && /* @__PURE__ */ jsx29("div", { className: "text-sm text-red-600 bg-red-50 p-3 rounded-md", children: error }),
|
|
2588
|
+
/* @__PURE__ */ jsx29(Button, { type: "submit", className: "w-full", disabled: isSubmitting, children: isSubmitting ? "Signing in..." : "Sign In" })
|
|
2589
|
+
] });
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
// src/components/auth/protected-route.tsx
|
|
2593
|
+
import { Fragment, jsx as jsx30 } from "react/jsx-runtime";
|
|
2594
|
+
function ProtectedRoute({ children, fallback, loadingFallback }) {
|
|
2595
|
+
const { isAuthenticated, isLoading } = useAuth();
|
|
2596
|
+
const disableAuth = typeof process !== "undefined" && process.env?.NEXT_PUBLIC_DISABLE_AUTH === "true";
|
|
2597
|
+
if (disableAuth) {
|
|
2598
|
+
return /* @__PURE__ */ jsx30(Fragment, { children });
|
|
2599
|
+
}
|
|
2600
|
+
if (isLoading) {
|
|
2601
|
+
return /* @__PURE__ */ jsx30(Fragment, { children: loadingFallback || /* @__PURE__ */ jsx30("div", { children: "Loading..." }) });
|
|
2602
|
+
}
|
|
2603
|
+
if (!isAuthenticated) {
|
|
2604
|
+
return /* @__PURE__ */ jsx30(Fragment, { children: fallback || /* @__PURE__ */ jsx30("div", { children: "Please sign in to access this page." }) });
|
|
2605
|
+
}
|
|
2606
|
+
return /* @__PURE__ */ jsx30(Fragment, { children });
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
// src/components/auth/oauth-button.tsx
|
|
2610
|
+
import { jsx as jsx31 } from "react/jsx-runtime";
|
|
2611
|
+
function OAuthButton({
|
|
2612
|
+
provider,
|
|
2613
|
+
apiBaseUrl,
|
|
2614
|
+
redirectTo,
|
|
2615
|
+
children,
|
|
2616
|
+
className
|
|
2617
|
+
}) {
|
|
2618
|
+
const handleClick = async () => {
|
|
2619
|
+
try {
|
|
2620
|
+
const response = await fetch(`${apiBaseUrl}/auth/oauth`, {
|
|
2621
|
+
method: "POST",
|
|
2622
|
+
headers: {
|
|
2623
|
+
"Content-Type": "application/json"
|
|
2624
|
+
},
|
|
2625
|
+
body: JSON.stringify({ provider, redirectTo })
|
|
2626
|
+
});
|
|
2627
|
+
if (!response.ok) {
|
|
2628
|
+
throw new Error("OAuth initialization failed");
|
|
2629
|
+
}
|
|
2630
|
+
const { url } = await response.json();
|
|
2631
|
+
window.location.href = url;
|
|
2632
|
+
} catch (error) {
|
|
2633
|
+
console.error("OAuth error:", error);
|
|
2634
|
+
}
|
|
2635
|
+
};
|
|
2636
|
+
const providerLabels = {
|
|
2637
|
+
google: "Google",
|
|
2638
|
+
github: "GitHub",
|
|
2639
|
+
gitlab: "GitLab",
|
|
2640
|
+
azure: "Azure",
|
|
2641
|
+
bitbucket: "Bitbucket"
|
|
2642
|
+
};
|
|
2643
|
+
return /* @__PURE__ */ jsx31(Button, { type: "button", variant: "outline", onClick: handleClick, className, children: children || `Sign in with ${providerLabels[provider]}` });
|
|
2644
|
+
}
|
|
2645
|
+
export {
|
|
2646
|
+
AdminLayout,
|
|
2647
|
+
AdminProvider,
|
|
2648
|
+
ArrayField,
|
|
2649
|
+
AuthProvider,
|
|
2650
|
+
Button,
|
|
2651
|
+
DashboardPage,
|
|
2652
|
+
Dialog,
|
|
2653
|
+
ErrorBoundary,
|
|
2654
|
+
FormGenerator,
|
|
2655
|
+
ImagePicker,
|
|
2656
|
+
Input,
|
|
2657
|
+
KpiCards,
|
|
2658
|
+
Label,
|
|
2659
|
+
LoginForm,
|
|
2660
|
+
MediaBrowser,
|
|
2661
|
+
NavigationEditor,
|
|
2662
|
+
OAuthButton,
|
|
2663
|
+
ObjectField,
|
|
2664
|
+
PageEditor,
|
|
2665
|
+
PageList,
|
|
2666
|
+
ProtectedRoute,
|
|
2667
|
+
QuickActions,
|
|
2668
|
+
RecentPages,
|
|
2669
|
+
RichTextEditor,
|
|
2670
|
+
SectionEditor,
|
|
2671
|
+
Skeleton,
|
|
2672
|
+
StringInput,
|
|
2673
|
+
StructCMSAdminApp,
|
|
2674
|
+
TextInput,
|
|
2675
|
+
Textarea,
|
|
2676
|
+
ToastProvider,
|
|
2677
|
+
buttonVariants,
|
|
2678
|
+
cn,
|
|
2679
|
+
fieldNameToLabel,
|
|
2680
|
+
resolveFieldType,
|
|
2681
|
+
useAdmin,
|
|
2682
|
+
useApiClient,
|
|
2683
|
+
useAuth,
|
|
2684
|
+
useToast
|
|
2685
|
+
};
|
|
2686
|
+
//# sourceMappingURL=index.js.map
|