@withwiz/gallery 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.
Files changed (77) hide show
  1. package/README.ko.md +484 -0
  2. package/README.md +484 -0
  3. package/dist/chunk-B3MQL6ZR.mjs +79 -0
  4. package/dist/chunk-B3MQL6ZR.mjs.map +1 -0
  5. package/dist/chunk-BC3TPJQY.mjs +122 -0
  6. package/dist/chunk-BC3TPJQY.mjs.map +1 -0
  7. package/dist/chunk-BTLELFKC.mjs +11 -0
  8. package/dist/chunk-BTLELFKC.mjs.map +1 -0
  9. package/dist/chunk-GDUGKVDQ.mjs +50 -0
  10. package/dist/chunk-GDUGKVDQ.mjs.map +1 -0
  11. package/dist/chunk-GFOWZGJX.js +50 -0
  12. package/dist/chunk-GFOWZGJX.js.map +1 -0
  13. package/dist/chunk-ILSZPLME.js +47 -0
  14. package/dist/chunk-ILSZPLME.js.map +1 -0
  15. package/dist/chunk-M7CCTDUG.js +122 -0
  16. package/dist/chunk-M7CCTDUG.js.map +1 -0
  17. package/dist/chunk-WVRTM6E7.js +19 -0
  18. package/dist/chunk-WVRTM6E7.js.map +1 -0
  19. package/dist/chunk-XLPMNJJV.js +11 -0
  20. package/dist/chunk-XLPMNJJV.js.map +1 -0
  21. package/dist/chunk-XSPWM4DD.js +79 -0
  22. package/dist/chunk-XSPWM4DD.js.map +1 -0
  23. package/dist/chunk-YUMB3GBN.mjs +47 -0
  24. package/dist/chunk-YUMB3GBN.mjs.map +1 -0
  25. package/dist/chunk-ZEZCUJJD.mjs +19 -0
  26. package/dist/chunk-ZEZCUJJD.mjs.map +1 -0
  27. package/dist/components/gallery.css +1074 -0
  28. package/dist/components/index.d.mts +156 -0
  29. package/dist/components/index.d.ts +156 -0
  30. package/dist/components/index.js +1215 -0
  31. package/dist/components/index.js.map +1 -0
  32. package/dist/components/index.mjs +1215 -0
  33. package/dist/components/index.mjs.map +1 -0
  34. package/dist/errors-B9gzUsX-.d.ts +39 -0
  35. package/dist/errors-DYrlDCzP.d.mts +39 -0
  36. package/dist/gallery.css +1074 -0
  37. package/dist/hooks/index.d.mts +65 -0
  38. package/dist/hooks/index.d.ts +65 -0
  39. package/dist/hooks/index.js +14 -0
  40. package/dist/hooks/index.js.map +1 -0
  41. package/dist/hooks/index.mjs +14 -0
  42. package/dist/hooks/index.mjs.map +1 -0
  43. package/dist/index.d.mts +17 -0
  44. package/dist/index.d.ts +17 -0
  45. package/dist/index.js +37 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/index.mjs +37 -0
  48. package/dist/index.mjs.map +1 -0
  49. package/dist/presets/ballet.d.mts +64 -0
  50. package/dist/presets/ballet.d.ts +64 -0
  51. package/dist/presets/ballet.js +162 -0
  52. package/dist/presets/ballet.js.map +1 -0
  53. package/dist/presets/ballet.mjs +162 -0
  54. package/dist/presets/ballet.mjs.map +1 -0
  55. package/dist/server/index.d.mts +157 -0
  56. package/dist/server/index.d.ts +157 -0
  57. package/dist/server/index.js +619 -0
  58. package/dist/server/index.js.map +1 -0
  59. package/dist/server/index.mjs +619 -0
  60. package/dist/server/index.mjs.map +1 -0
  61. package/dist/types/index.d.mts +137 -0
  62. package/dist/types/index.d.ts +137 -0
  63. package/dist/types/index.js +1 -0
  64. package/dist/types/index.js.map +1 -0
  65. package/dist/types/index.mjs +1 -0
  66. package/dist/types/index.mjs.map +1 -0
  67. package/dist/useGalleryLightbox-BYzPyBYB.d.mts +22 -0
  68. package/dist/useGalleryLightbox-BYzPyBYB.d.ts +22 -0
  69. package/dist/validators/index.d.mts +68 -0
  70. package/dist/validators/index.d.ts +68 -0
  71. package/dist/validators/index.js +7 -0
  72. package/dist/validators/index.js.map +1 -0
  73. package/dist/validators/index.mjs +7 -0
  74. package/dist/validators/index.mjs.map +1 -0
  75. package/package.json +85 -0
  76. package/prisma/gallery.schema.prisma +62 -0
  77. package/prisma/migrations/2026-05-24-enum-to-table.sql +65 -0
@@ -0,0 +1,1215 @@
1
+ "use client";
2
+ import {
3
+ getGalleryConfig
4
+ } from "../chunk-ZEZCUJJD.mjs";
5
+ import {
6
+ useImageDropZone
7
+ } from "../chunk-BC3TPJQY.mjs";
8
+ import {
9
+ cn
10
+ } from "../chunk-BTLELFKC.mjs";
11
+
12
+ // src/components/ToggleSwitch.tsx
13
+ import { jsx, jsxs } from "react/jsx-runtime";
14
+ function ToggleSwitch(props) {
15
+ const { checked, onChange, disabled = false, label, size = "md", className } = props;
16
+ const handleToggle = () => {
17
+ if (disabled) return;
18
+ onChange(!checked);
19
+ };
20
+ const handleKeyDown = (e) => {
21
+ if (disabled) return;
22
+ if (e.key === " " || e.key === "Enter") {
23
+ e.preventDefault();
24
+ onChange(!checked);
25
+ }
26
+ };
27
+ return /* @__PURE__ */ jsxs("span", { className: cn("gallery-toggle", `gallery-toggle--${size}`, className), children: [
28
+ /* @__PURE__ */ jsx(
29
+ "button",
30
+ {
31
+ type: "button",
32
+ role: "switch",
33
+ "aria-checked": checked,
34
+ "aria-disabled": disabled || void 0,
35
+ "aria-label": label,
36
+ disabled,
37
+ onClick: handleToggle,
38
+ onKeyDown: handleKeyDown,
39
+ className: cn(
40
+ "gallery-toggle__track",
41
+ checked && "gallery-toggle--checked",
42
+ disabled && "gallery-toggle--disabled"
43
+ ),
44
+ children: /* @__PURE__ */ jsx("span", { className: "gallery-toggle__thumb", "aria-hidden": "true" })
45
+ }
46
+ ),
47
+ label ? /* @__PURE__ */ jsx("span", { className: "gallery-toggle__label", children: label }) : null
48
+ ] });
49
+ }
50
+
51
+ // src/components/ImageDropZone.tsx
52
+ import { useRef } from "react";
53
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
54
+ var DEFAULT_TEXT = {
55
+ dropPrompt: "Drop image here or click to browse",
56
+ replacePrompt: "Drop a new image to replace",
57
+ rejected: "Some files were rejected"
58
+ };
59
+ function ImageDropZone(props) {
60
+ const {
61
+ onFiles,
62
+ accept,
63
+ maxSize,
64
+ multiple,
65
+ disabled = false,
66
+ previewUrl,
67
+ i18n,
68
+ className
69
+ } = props;
70
+ const inputRef = useRef(null);
71
+ const { isDragging, dropProps, inputProps, rejectedReasons } = useImageDropZone({
72
+ onFiles,
73
+ accept,
74
+ maxSize,
75
+ multiple,
76
+ disabled
77
+ });
78
+ const text = {
79
+ dropPrompt: i18n?.dropPrompt ?? DEFAULT_TEXT.dropPrompt,
80
+ replacePrompt: i18n?.replacePrompt ?? DEFAULT_TEXT.replacePrompt,
81
+ rejected: i18n?.rejected ?? DEFAULT_TEXT.rejected
82
+ };
83
+ const openPicker = () => {
84
+ if (disabled) return;
85
+ inputRef.current?.click();
86
+ };
87
+ const containerClass = cn(
88
+ "gallery-dropzone",
89
+ isDragging && "gallery-dropzone--dragging",
90
+ disabled && "gallery-dropzone--disabled",
91
+ previewUrl && "gallery-dropzone--has-preview",
92
+ className
93
+ );
94
+ return /* @__PURE__ */ jsxs2("div", { className: "gallery-dropzone-wrap", children: [
95
+ /* @__PURE__ */ jsx2(
96
+ "input",
97
+ {
98
+ ref: inputRef,
99
+ ...inputProps,
100
+ disabled,
101
+ className: "gallery-dropzone__input",
102
+ style: { position: "absolute", width: 1, height: 1, opacity: 0, pointerEvents: "none" },
103
+ "aria-hidden": "true",
104
+ tabIndex: -1
105
+ }
106
+ ),
107
+ /* @__PURE__ */ jsx2(
108
+ "div",
109
+ {
110
+ className: containerClass,
111
+ onClick: openPicker,
112
+ role: "button",
113
+ tabIndex: disabled ? -1 : 0,
114
+ "aria-disabled": disabled || void 0,
115
+ onKeyDown: (e) => {
116
+ if (disabled) return;
117
+ if (e.key === "Enter" || e.key === " ") {
118
+ e.preventDefault();
119
+ openPicker();
120
+ }
121
+ },
122
+ ...dropProps,
123
+ children: previewUrl ? /* @__PURE__ */ jsxs2(Fragment, { children: [
124
+ /* @__PURE__ */ jsx2(
125
+ "img",
126
+ {
127
+ src: previewUrl,
128
+ alt: "",
129
+ className: "gallery-dropzone__preview"
130
+ }
131
+ ),
132
+ /* @__PURE__ */ jsx2("div", { className: "gallery-dropzone__prompt gallery-dropzone__prompt--overlay", children: text.replacePrompt })
133
+ ] }) : /* @__PURE__ */ jsx2("div", { className: "gallery-dropzone__prompt", children: text.dropPrompt })
134
+ }
135
+ ),
136
+ rejectedReasons.length > 0 ? /* @__PURE__ */ jsxs2("ul", { className: "gallery-dropzone__rejected", role: "alert", children: [
137
+ /* @__PURE__ */ jsx2("li", { className: "gallery-dropzone__rejected-title", children: text.rejected }),
138
+ rejectedReasons.map((reason, i) => /* @__PURE__ */ jsx2("li", { className: "gallery-dropzone__rejected-item", children: reason }, i))
139
+ ] }) : null
140
+ ] });
141
+ }
142
+
143
+ // src/components/GalleryManagerLayout.tsx
144
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
145
+ var DEFAULT_TEXT2 = {
146
+ "admin.emptyState": "Select an item to edit, or create a new one."
147
+ };
148
+ function t(i18n, key) {
149
+ return i18n?.[key] ?? DEFAULT_TEXT2[key] ?? key;
150
+ }
151
+ function GalleryManagerLayout(props) {
152
+ const {
153
+ listRoot,
154
+ filterControls,
155
+ toolbar,
156
+ editForm,
157
+ emptyState,
158
+ homePreview,
159
+ className,
160
+ i18n,
161
+ ui
162
+ } = props;
163
+ const slots = ui?.slots ?? {};
164
+ const classes = ui?.classNames ?? {};
165
+ const toolbarContent = slots.managerToolbar ?? toolbar;
166
+ const listContent = slots.listRoot ?? listRoot;
167
+ const editContent = slots.editFormRoot ?? editForm;
168
+ const previewContent = slots.homePreviewRoot ?? homePreview;
169
+ const emptyContent = slots.emptyState ?? emptyState ?? /* @__PURE__ */ jsx3("span", { children: t(i18n, "admin.emptyState") });
170
+ const hasPreview = previewContent !== void 0 && previewContent !== null;
171
+ return /* @__PURE__ */ jsxs3(
172
+ "div",
173
+ {
174
+ className: cn(
175
+ "gallery-manager",
176
+ hasPreview ? "gallery-manager--with-preview" : "gallery-manager--no-preview",
177
+ classes.managerRoot,
178
+ className
179
+ ),
180
+ children: [
181
+ /* @__PURE__ */ jsxs3("aside", { className: cn("gallery-manager__list", classes.listRoot), children: [
182
+ toolbarContent ? /* @__PURE__ */ jsx3("div", { className: cn("gallery-manager__toolbar", classes.managerToolbar), children: toolbarContent }) : null,
183
+ filterControls ? /* @__PURE__ */ jsx3("div", { className: "gallery-manager__filter", children: filterControls }) : null,
184
+ /* @__PURE__ */ jsx3("div", { className: "gallery-manager__list-body", children: listContent })
185
+ ] }),
186
+ /* @__PURE__ */ jsx3("section", { className: cn("gallery-manager__form", classes.editFormRoot), children: editContent !== null && editContent !== void 0 ? editContent : /* @__PURE__ */ jsx3("div", { className: cn("gallery-manager__empty", classes.emptyState), children: emptyContent }) }),
187
+ hasPreview ? /* @__PURE__ */ jsx3("aside", { className: cn("gallery-manager__preview", classes.homePreviewRoot), children: previewContent }) : null
188
+ ]
189
+ }
190
+ );
191
+ }
192
+
193
+ // src/components/GalleryEditForm.tsx
194
+ import {
195
+ useCallback,
196
+ useEffect,
197
+ useMemo,
198
+ useRef as useRef2,
199
+ useState
200
+ } from "react";
201
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
202
+ var DEFAULT_TEXT3 = {
203
+ "form.caption": "Caption",
204
+ "form.captionPlaceholder": "Image description (optional)",
205
+ "form.category": "Category",
206
+ "form.sortOrder": "Sort order",
207
+ "form.published": "Published",
208
+ "form.featured": "Featured",
209
+ "form.save": "Save",
210
+ "form.cancel": "Cancel",
211
+ "form.imageDropPrompt": "Drop image here or click to browse",
212
+ "form.imageReplacePrompt": "Drop a new image to replace",
213
+ "admin.featuredOverLimit": "Featured cap reached \u2014 uncheck others first"
214
+ };
215
+ function t2(i18n, key) {
216
+ return i18n?.[key] ?? DEFAULT_TEXT3[key] ?? key;
217
+ }
218
+ function buildInitialForm(value, categories) {
219
+ if (value) {
220
+ return {
221
+ imageUrl: value.imageUrl,
222
+ imageKey: value.imageKey ?? "",
223
+ caption: value.caption ?? "",
224
+ categoryId: value.categoryId,
225
+ sortOrder: value.sortOrder,
226
+ featured: value.featured,
227
+ published: value.published
228
+ };
229
+ }
230
+ return {
231
+ imageUrl: "",
232
+ imageKey: "",
233
+ caption: "",
234
+ categoryId: categories[0]?.id ?? "",
235
+ sortOrder: 0,
236
+ featured: false,
237
+ published: false
238
+ };
239
+ }
240
+ function GalleryEditForm(props) {
241
+ const {
242
+ value,
243
+ categories,
244
+ onSubmit,
245
+ onCancel,
246
+ onDelete,
247
+ multipleMode = false,
248
+ onSubmitMany,
249
+ canToggleFeatured = true,
250
+ saving = false,
251
+ onImageSelect,
252
+ i18n,
253
+ className
254
+ } = props;
255
+ const isNew = value === null;
256
+ const [form, setForm] = useState(() => buildInitialForm(value, categories));
257
+ const [multiImages, setMultiImages] = useState([]);
258
+ const [imageError, setImageError] = useState(null);
259
+ const valueIdRef = useRef2(value?.id ?? null);
260
+ const dirtyRef = useRef2(false);
261
+ useEffect(() => {
262
+ const newId = value?.id ?? null;
263
+ if (newId !== valueIdRef.current) {
264
+ valueIdRef.current = newId;
265
+ if (!dirtyRef.current) {
266
+ setForm(buildInitialForm(value, categories));
267
+ }
268
+ } else if (value && !dirtyRef.current) {
269
+ setForm(buildInitialForm(value, categories));
270
+ }
271
+ }, [value, categories]);
272
+ useEffect(() => {
273
+ if (!form.categoryId && categories.length > 0) {
274
+ setForm((f) => ({ ...f, categoryId: categories[0].id }));
275
+ }
276
+ }, [categories, form.categoryId]);
277
+ const isMultiActive = isNew && multipleMode && multiImages.length > 1;
278
+ const featuredDisabled = !canToggleFeatured && !form.featured;
279
+ const updateField = useCallback((key, val) => {
280
+ dirtyRef.current = true;
281
+ setForm((prev) => ({ ...prev, [key]: val }));
282
+ }, []);
283
+ const handleFiles = useCallback(
284
+ async (files) => {
285
+ if (files.length === 0) return;
286
+ setImageError(null);
287
+ if (!onImageSelect) {
288
+ setImageError("onImageSelect prop missing \u2014 host must provide upload handler");
289
+ return;
290
+ }
291
+ try {
292
+ if (isNew && multipleMode) {
293
+ const uploaded = await Promise.all(files.map((f) => onImageSelect(f)));
294
+ setMultiImages((prev) => {
295
+ const next = [...prev, ...uploaded];
296
+ setForm((f) => {
297
+ if (!f.imageUrl && next.length > 0) {
298
+ return { ...f, imageUrl: next[0].url, imageKey: next[0].key ?? "" };
299
+ }
300
+ return f;
301
+ });
302
+ return next;
303
+ });
304
+ } else {
305
+ const r = await onImageSelect(files[0]);
306
+ setForm((f) => ({ ...f, imageUrl: r.url, imageKey: r.key ?? "" }));
307
+ }
308
+ } catch (err) {
309
+ setImageError(err instanceof Error ? err.message : "Image upload failed");
310
+ }
311
+ },
312
+ [isNew, multipleMode, onImageSelect]
313
+ );
314
+ const handleSubmit = useCallback(async () => {
315
+ if (isMultiActive && onSubmitMany) {
316
+ const items = multiImages.map((img, i) => ({
317
+ imageUrl: img.url,
318
+ imageKey: img.key || void 0,
319
+ caption: form.caption ? `${form.caption}_${i + 1}` : void 0,
320
+ categoryId: form.categoryId,
321
+ sortOrder: form.sortOrder + i,
322
+ featured: form.featured,
323
+ published: form.published
324
+ }));
325
+ await onSubmitMany(items);
326
+ return;
327
+ }
328
+ if (!form.imageUrl) {
329
+ setImageError("Image is required");
330
+ return;
331
+ }
332
+ const payload = {
333
+ imageUrl: form.imageUrl,
334
+ imageKey: form.imageKey || void 0,
335
+ caption: form.caption || void 0,
336
+ categoryId: form.categoryId,
337
+ sortOrder: form.sortOrder,
338
+ featured: form.featured,
339
+ published: form.published
340
+ };
341
+ await onSubmit(payload);
342
+ }, [form, isMultiActive, multiImages, onSubmit, onSubmitMany]);
343
+ const captionPreview = useMemo(() => {
344
+ if (!isMultiActive || !form.caption) return null;
345
+ return multiImages.slice(0, 3).map((_, i) => `${form.caption}_${i + 1}`).join(", ") + (multiImages.length > 3 ? ", ..." : "");
346
+ }, [isMultiActive, multiImages, form.caption]);
347
+ return /* @__PURE__ */ jsxs4("div", { className: cn("gallery-edit-form", className), children: [
348
+ /* @__PURE__ */ jsxs4("header", { className: "gallery-edit-form__header", children: [
349
+ /* @__PURE__ */ jsx4("h2", { className: "gallery-edit-form__title", children: isNew ? isMultiActive ? `Batch (${multiImages.length})` : "New image" : "Edit image" }),
350
+ /* @__PURE__ */ jsxs4("div", { className: "gallery-edit-form__actions", "data-toggle-group": true, children: [
351
+ /* @__PURE__ */ jsx4("span", { "data-toggle": "featured", children: /* @__PURE__ */ jsx4(
352
+ ToggleSwitch,
353
+ {
354
+ checked: form.featured,
355
+ onChange: (v) => updateField("featured", v),
356
+ label: t2(i18n, "form.featured"),
357
+ disabled: featuredDisabled,
358
+ size: "sm"
359
+ }
360
+ ) }),
361
+ featuredDisabled ? /* @__PURE__ */ jsx4("span", { className: "gallery-edit-form__note", children: t2(i18n, "admin.featuredOverLimit") }) : null,
362
+ /* @__PURE__ */ jsx4("span", { "data-toggle": "published", children: /* @__PURE__ */ jsx4(
363
+ ToggleSwitch,
364
+ {
365
+ checked: form.published,
366
+ onChange: (v) => updateField("published", v),
367
+ label: t2(i18n, "form.published"),
368
+ size: "sm"
369
+ }
370
+ ) }),
371
+ !isNew && onDelete ? /* @__PURE__ */ jsx4(
372
+ "button",
373
+ {
374
+ type: "button",
375
+ className: "gallery-edit-form__btn gallery-edit-form__btn--danger",
376
+ onClick: () => void onDelete(),
377
+ children: "Delete"
378
+ }
379
+ ) : null,
380
+ onCancel ? /* @__PURE__ */ jsx4(
381
+ "button",
382
+ {
383
+ type: "button",
384
+ className: "gallery-edit-form__btn",
385
+ onClick: onCancel,
386
+ children: t2(i18n, "form.cancel")
387
+ }
388
+ ) : null,
389
+ /* @__PURE__ */ jsx4(
390
+ "button",
391
+ {
392
+ type: "button",
393
+ className: "gallery-edit-form__btn gallery-edit-form__btn--primary",
394
+ onClick: () => void handleSubmit(),
395
+ disabled: saving,
396
+ children: saving ? "Saving..." : isMultiActive ? `Save ${multiImages.length}` : t2(i18n, "form.save")
397
+ }
398
+ )
399
+ ] })
400
+ ] }),
401
+ /* @__PURE__ */ jsxs4("div", { className: "gallery-edit-form__section", children: [
402
+ /* @__PURE__ */ jsx4("div", { className: "gallery-edit-form__section-title", children: "Image" }),
403
+ /* @__PURE__ */ jsx4(
404
+ ImageDropZone,
405
+ {
406
+ onFiles: handleFiles,
407
+ previewUrl: form.imageUrl || null,
408
+ multiple: isNew && multipleMode,
409
+ i18n: {
410
+ dropPrompt: t2(i18n, "form.imageDropPrompt"),
411
+ replacePrompt: t2(i18n, "form.imageReplacePrompt")
412
+ }
413
+ }
414
+ ),
415
+ imageError ? /* @__PURE__ */ jsx4("div", { className: "gallery-edit-form__error", role: "alert", children: imageError }) : null,
416
+ isMultiActive ? /* @__PURE__ */ jsx4("div", { className: "gallery-edit-form__multi-grid", children: multiImages.map((img, i) => /* @__PURE__ */ jsxs4("div", { className: "gallery-edit-form__multi-tile", children: [
417
+ /* @__PURE__ */ jsx4("img", { src: img.url, alt: "", className: "gallery-edit-form__multi-img" }),
418
+ /* @__PURE__ */ jsx4(
419
+ "button",
420
+ {
421
+ type: "button",
422
+ className: "gallery-edit-form__multi-remove",
423
+ onClick: () => {
424
+ setMultiImages((prev) => {
425
+ const next = prev.filter((_, idx) => idx !== i);
426
+ setForm((f) => {
427
+ if (next.length > 0) {
428
+ return { ...f, imageUrl: next[0].url, imageKey: next[0].key ?? "" };
429
+ }
430
+ return { ...f, imageUrl: "", imageKey: "" };
431
+ });
432
+ return next;
433
+ });
434
+ },
435
+ "aria-label": "remove",
436
+ children: "\xD7"
437
+ }
438
+ )
439
+ ] }, img.key ?? `mi-${i}`)) }) : null
440
+ ] }),
441
+ /* @__PURE__ */ jsxs4("div", { className: "gallery-edit-form__section", children: [
442
+ /* @__PURE__ */ jsx4("div", { className: "gallery-edit-form__section-title", children: t2(i18n, "form.caption") }),
443
+ /* @__PURE__ */ jsx4(
444
+ "input",
445
+ {
446
+ type: "text",
447
+ name: "caption",
448
+ className: "gallery-edit-form__input",
449
+ value: form.caption,
450
+ maxLength: 200,
451
+ placeholder: t2(i18n, "form.captionPlaceholder"),
452
+ onChange: (e) => updateField("caption", e.target.value)
453
+ }
454
+ ),
455
+ captionPreview ? /* @__PURE__ */ jsxs4("div", { className: "gallery-edit-form__hint", children: [
456
+ "Preview: ",
457
+ captionPreview
458
+ ] }) : null
459
+ ] }),
460
+ /* @__PURE__ */ jsxs4("div", { className: "gallery-edit-form__row", children: [
461
+ /* @__PURE__ */ jsxs4("div", { className: "gallery-edit-form__section", children: [
462
+ /* @__PURE__ */ jsx4("div", { className: "gallery-edit-form__section-title", children: t2(i18n, "form.category") }),
463
+ /* @__PURE__ */ jsx4(
464
+ "select",
465
+ {
466
+ name: "categoryId",
467
+ className: "gallery-edit-form__input",
468
+ value: form.categoryId,
469
+ onChange: (e) => updateField("categoryId", e.target.value),
470
+ children: categories.map((c) => /* @__PURE__ */ jsx4("option", { value: c.id, children: c.labelKo }, c.id))
471
+ }
472
+ )
473
+ ] }),
474
+ /* @__PURE__ */ jsxs4("div", { className: "gallery-edit-form__section", children: [
475
+ /* @__PURE__ */ jsx4("div", { className: "gallery-edit-form__section-title", children: t2(i18n, "form.sortOrder") }),
476
+ /* @__PURE__ */ jsx4(
477
+ "input",
478
+ {
479
+ type: "number",
480
+ name: "sortOrder",
481
+ className: "gallery-edit-form__input",
482
+ value: form.sortOrder,
483
+ min: 0,
484
+ onChange: (e) => {
485
+ const n = parseInt(e.target.value, 10);
486
+ updateField("sortOrder", Number.isNaN(n) ? 0 : n);
487
+ }
488
+ }
489
+ ),
490
+ isMultiActive ? /* @__PURE__ */ jsxs4("div", { className: "gallery-edit-form__hint", children: [
491
+ form.sortOrder,
492
+ " ~ ",
493
+ form.sortOrder + multiImages.length - 1
494
+ ] }) : null
495
+ ] })
496
+ ] })
497
+ ] });
498
+ }
499
+
500
+ // src/components/GalleryHomePreview.tsx
501
+ import { useCallback as useCallback2, useRef as useRef3, useState as useState2 } from "react";
502
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
503
+ var DEFAULT_TEXT4 = {
504
+ "preview.title": "Home preview",
505
+ "preview.dragHint": "Drag tiles to reorder",
506
+ "preview.emptyState": "Select images to feature on the home page"
507
+ };
508
+ function t3(i18n, key) {
509
+ return i18n?.[key] ?? DEFAULT_TEXT4[key] ?? key;
510
+ }
511
+ function GalleryHomePreview(props) {
512
+ const { items, maxCount, onReorder, onToggleFeatured, i18n, className } = props;
513
+ const [dragIndex, setDragIndex] = useState2(null);
514
+ const [dragOverIndex, setDragOverIndex] = useState2(null);
515
+ const reorderedRef = useRef3(null);
516
+ const handleDragStart = useCallback2(
517
+ (e, index) => {
518
+ setDragIndex(index);
519
+ try {
520
+ e.dataTransfer.effectAllowed = "move";
521
+ } catch {
522
+ }
523
+ },
524
+ []
525
+ );
526
+ const handleDragOver = useCallback2(
527
+ (e, index) => {
528
+ e.preventDefault();
529
+ try {
530
+ e.dataTransfer.dropEffect = "move";
531
+ } catch {
532
+ }
533
+ setDragOverIndex(index);
534
+ },
535
+ []
536
+ );
537
+ const handleDrop = useCallback2(
538
+ (e, dropIndex) => {
539
+ e.preventDefault();
540
+ if (dragIndex === null || dragIndex === dropIndex) {
541
+ reorderedRef.current = null;
542
+ return;
543
+ }
544
+ const reordered = [...items];
545
+ const tmp = reordered[dragIndex];
546
+ reordered[dragIndex] = reordered[dropIndex];
547
+ reordered[dropIndex] = tmp;
548
+ reorderedRef.current = reordered.map((it) => it.id);
549
+ },
550
+ [dragIndex, items]
551
+ );
552
+ const handleDragEnd = useCallback2(() => {
553
+ if (reorderedRef.current) {
554
+ void onReorder(reorderedRef.current);
555
+ }
556
+ reorderedRef.current = null;
557
+ setDragIndex(null);
558
+ setDragOverIndex(null);
559
+ }, [onReorder]);
560
+ const handleStarClick = useCallback2(
561
+ (item) => {
562
+ void onToggleFeatured(item.id, !item.featured);
563
+ },
564
+ [onToggleFeatured]
565
+ );
566
+ if (items.length === 0) {
567
+ return /* @__PURE__ */ jsxs5("div", { className: cn("gallery-home-preview", "gallery-home-preview--empty", className), children: [
568
+ /* @__PURE__ */ jsx5("div", { className: "gallery-home-preview__header", children: /* @__PURE__ */ jsx5("h3", { className: "gallery-home-preview__title", children: t3(i18n, "preview.title") }) }),
569
+ /* @__PURE__ */ jsx5("div", { className: "gallery-home-preview__empty", children: t3(i18n, "preview.emptyState") })
570
+ ] });
571
+ }
572
+ return /* @__PURE__ */ jsxs5("div", { className: cn("gallery-home-preview", className), children: [
573
+ /* @__PURE__ */ jsxs5("div", { className: "gallery-home-preview__header", children: [
574
+ /* @__PURE__ */ jsx5("h3", { className: "gallery-home-preview__title", children: t3(i18n, "preview.title") }),
575
+ /* @__PURE__ */ jsxs5("span", { className: "gallery-home-preview__count", children: [
576
+ items.length,
577
+ "/",
578
+ maxCount
579
+ ] })
580
+ ] }),
581
+ /* @__PURE__ */ jsx5("div", { className: "gallery-home-preview__hint", children: t3(i18n, "preview.dragHint") }),
582
+ /* @__PURE__ */ jsx5(
583
+ "div",
584
+ {
585
+ className: "gallery-home-preview__mosaic",
586
+ "data-count": maxCount,
587
+ children: Array.from({ length: maxCount }).map((_, slotIndex) => {
588
+ const item = items[slotIndex];
589
+ if (!item) {
590
+ return /* @__PURE__ */ jsx5(
591
+ "div",
592
+ {
593
+ className: "gallery-home-preview__tile gallery-home-preview__tile--empty",
594
+ children: /* @__PURE__ */ jsx5("span", { className: "gallery-home-preview__order", children: slotIndex + 1 })
595
+ },
596
+ `empty-${slotIndex}`
597
+ );
598
+ }
599
+ return /* @__PURE__ */ jsxs5(
600
+ "div",
601
+ {
602
+ className: cn(
603
+ "gallery-home-preview__tile",
604
+ dragOverIndex === slotIndex && "gallery-home-preview__tile--drag-over",
605
+ dragIndex === slotIndex && "gallery-home-preview__tile--dragging"
606
+ ),
607
+ draggable: true,
608
+ onDragStart: (e) => handleDragStart(e, slotIndex),
609
+ onDragOver: (e) => handleDragOver(e, slotIndex),
610
+ onDrop: (e) => handleDrop(e, slotIndex),
611
+ onDragEnd: handleDragEnd,
612
+ children: [
613
+ /* @__PURE__ */ jsx5(
614
+ "img",
615
+ {
616
+ src: item.imageUrl,
617
+ alt: item.caption ?? "",
618
+ className: "gallery-home-preview__img"
619
+ }
620
+ ),
621
+ /* @__PURE__ */ jsx5(
622
+ "button",
623
+ {
624
+ type: "button",
625
+ className: cn(
626
+ "gallery-home-preview__star",
627
+ item.featured && "gallery-home-preview__star--active"
628
+ ),
629
+ onClick: () => handleStarClick(item),
630
+ "aria-label": item.featured ? "unfeature" : "feature",
631
+ "aria-pressed": item.featured,
632
+ children: item.featured ? "\u2605" : "\u2606"
633
+ }
634
+ ),
635
+ /* @__PURE__ */ jsx5("span", { className: "gallery-home-preview__order", children: slotIndex + 1 }),
636
+ item.caption ? /* @__PURE__ */ jsx5("div", { className: "gallery-home-preview__caption", children: item.caption }) : null
637
+ ]
638
+ },
639
+ item.id
640
+ );
641
+ })
642
+ }
643
+ )
644
+ ] });
645
+ }
646
+
647
+ // src/components/GalleryAdminManager.tsx
648
+ import {
649
+ useCallback as useCallback3,
650
+ useEffect as useEffect2,
651
+ useMemo as useMemo2,
652
+ useRef as useRef4,
653
+ useState as useState3
654
+ } from "react";
655
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
656
+ async function clientFetch(url, init) {
657
+ return fetch(url, {
658
+ credentials: "include",
659
+ headers: init?.body ? { "Content-Type": "application/json", ...init?.headers } : init?.headers,
660
+ ...init
661
+ });
662
+ }
663
+ async function jsonOrNull(res) {
664
+ try {
665
+ return await res.json();
666
+ } catch {
667
+ return null;
668
+ }
669
+ }
670
+ var DEFAULT_TEXT5 = {
671
+ "admin.title": "Gallery",
672
+ "admin.newButton": "+ New",
673
+ "admin.searchPlaceholder": "Search...",
674
+ "admin.emptyState": "Select an item to edit"
675
+ };
676
+ function t4(i18n, key) {
677
+ return i18n?.[key] ?? DEFAULT_TEXT5[key] ?? key;
678
+ }
679
+ function GalleryAdminManager(props) {
680
+ const { initialMode, initialSelectedId, className } = props;
681
+ const config = getGalleryConfig();
682
+ const i18n = config.i18n ?? {};
683
+ const ui = config.ui;
684
+ const maxFeatured = config.limits.maxFeatured;
685
+ const mosaicCount = config.limits.mosaicCount;
686
+ const [items, setItems] = useState3([]);
687
+ const [categories, setCategories] = useState3([]);
688
+ const [selectedId, setSelectedId] = useState3(initialSelectedId ?? null);
689
+ const [mode, setMode] = useState3(
690
+ initialMode ?? (initialSelectedId ? "edit" : "list")
691
+ );
692
+ const [saving, setSaving] = useState3(false);
693
+ const [search, setSearch] = useState3("");
694
+ const [refreshKey, setRefreshKey] = useState3(0);
695
+ const mountedRef = useRef4(true);
696
+ useEffect2(() => {
697
+ mountedRef.current = true;
698
+ return () => {
699
+ mountedRef.current = false;
700
+ };
701
+ }, []);
702
+ useEffect2(() => {
703
+ let cancelled = false;
704
+ (async () => {
705
+ try {
706
+ const res = await clientFetch("/api/admin/galleries");
707
+ const json = await jsonOrNull(res);
708
+ if (!cancelled && mountedRef.current) {
709
+ const data = json?.data ?? json ?? [];
710
+ setItems(Array.isArray(data) ? data : []);
711
+ }
712
+ } catch {
713
+ }
714
+ })();
715
+ return () => {
716
+ cancelled = true;
717
+ };
718
+ }, [refreshKey]);
719
+ useEffect2(() => {
720
+ let cancelled = false;
721
+ (async () => {
722
+ try {
723
+ const res = await clientFetch("/api/admin/gallery-categories");
724
+ const json = await jsonOrNull(res);
725
+ if (!cancelled && mountedRef.current) {
726
+ const data = json?.data ?? json ?? [];
727
+ setCategories(Array.isArray(data) ? data : []);
728
+ }
729
+ } catch {
730
+ }
731
+ })();
732
+ return () => {
733
+ cancelled = true;
734
+ };
735
+ }, []);
736
+ const selectedItem = useMemo2(() => {
737
+ if (!selectedId) return null;
738
+ return items.find((i) => i.id === selectedId) ?? null;
739
+ }, [selectedId, items]);
740
+ const filteredItems = useMemo2(() => {
741
+ if (!search.trim()) return items;
742
+ const q = search.trim().toLowerCase();
743
+ return items.filter((g) => (g.caption ?? "").toLowerCase().includes(q));
744
+ }, [items, search]);
745
+ const featuredCount = useMemo2(
746
+ () => items.filter((i) => i.featured && i.published).length,
747
+ [items]
748
+ );
749
+ const previewItems = useMemo2(
750
+ () => items.filter((i) => i.featured && i.published).sort((a, b) => a.sortOrder - b.sortOrder),
751
+ [items]
752
+ );
753
+ const refresh = useCallback3(() => setRefreshKey((k) => k + 1), []);
754
+ const handleSelect = useCallback3((id) => {
755
+ setSelectedId(id);
756
+ setMode("edit");
757
+ }, []);
758
+ const handleNewClick = useCallback3(() => {
759
+ setSelectedId(null);
760
+ setMode("new");
761
+ }, []);
762
+ const handleCancel = useCallback3(() => {
763
+ setSelectedId(null);
764
+ setMode("list");
765
+ }, []);
766
+ const handleSubmit = useCallback3(
767
+ async (data) => {
768
+ setSaving(true);
769
+ try {
770
+ if (mode === "new") {
771
+ await clientFetch("/api/admin/galleries", {
772
+ method: "POST",
773
+ body: JSON.stringify(data)
774
+ });
775
+ } else if (selectedId) {
776
+ await clientFetch(`/api/admin/galleries/${selectedId}`, {
777
+ method: "PUT",
778
+ body: JSON.stringify(data)
779
+ });
780
+ }
781
+ if (mountedRef.current) {
782
+ refresh();
783
+ setMode("list");
784
+ setSelectedId(null);
785
+ }
786
+ } finally {
787
+ if (mountedRef.current) setSaving(false);
788
+ }
789
+ },
790
+ [mode, selectedId, refresh]
791
+ );
792
+ const handleSubmitMany = useCallback3(
793
+ async (batch) => {
794
+ setSaving(true);
795
+ try {
796
+ await clientFetch("/api/admin/galleries/bulk", {
797
+ method: "POST",
798
+ body: JSON.stringify({ items: batch })
799
+ });
800
+ if (mountedRef.current) {
801
+ refresh();
802
+ setMode("list");
803
+ }
804
+ } finally {
805
+ if (mountedRef.current) setSaving(false);
806
+ }
807
+ },
808
+ [refresh]
809
+ );
810
+ const handleDelete = useCallback3(async () => {
811
+ if (!selectedId) return;
812
+ if (typeof window !== "undefined" && !window.confirm("Delete this item?")) return;
813
+ await clientFetch(`/api/admin/galleries/${selectedId}`, { method: "DELETE" });
814
+ if (mountedRef.current) {
815
+ setSelectedId(null);
816
+ setMode("list");
817
+ refresh();
818
+ }
819
+ }, [selectedId, refresh]);
820
+ const handleToggleFeatured = useCallback3(
821
+ async (id, next) => {
822
+ if (next && featuredCount >= maxFeatured) {
823
+ if (typeof window !== "undefined") {
824
+ window.alert(t4(i18n, "admin.featuredOverLimit"));
825
+ }
826
+ return;
827
+ }
828
+ await clientFetch(`/api/admin/galleries/${id}`, {
829
+ method: "PUT",
830
+ body: JSON.stringify({ featured: next })
831
+ });
832
+ if (mountedRef.current) refresh();
833
+ },
834
+ [featuredCount, maxFeatured, i18n, refresh]
835
+ );
836
+ const handleReorder = useCallback3(
837
+ async (orderedIds) => {
838
+ await Promise.all(
839
+ orderedIds.map(
840
+ (id, index) => clientFetch(`/api/admin/galleries/${id}`, {
841
+ method: "PUT",
842
+ body: JSON.stringify({ sortOrder: index + 1 })
843
+ })
844
+ )
845
+ );
846
+ if (mountedRef.current) refresh();
847
+ },
848
+ [refresh]
849
+ );
850
+ const isMultipleMode = mode === "new";
851
+ const editForm = mode === "list" ? null : /* @__PURE__ */ jsx6(
852
+ GalleryEditForm,
853
+ {
854
+ value: mode === "edit" ? selectedItem : null,
855
+ categories,
856
+ saving,
857
+ multipleMode: isMultipleMode,
858
+ canToggleFeatured: featuredCount < maxFeatured || (selectedItem?.featured ?? false),
859
+ i18n,
860
+ onSubmit: handleSubmit,
861
+ onSubmitMany: handleSubmitMany,
862
+ onCancel: handleCancel,
863
+ onDelete: mode === "edit" ? handleDelete : void 0
864
+ },
865
+ mode === "new" ? "new" : selectedId ?? "edit"
866
+ );
867
+ const filterControls = /* @__PURE__ */ jsxs6("div", { className: "gallery-manager__filter-controls", children: [
868
+ /* @__PURE__ */ jsx6(
869
+ "input",
870
+ {
871
+ type: "search",
872
+ className: "gallery-manager__search",
873
+ placeholder: t4(i18n, "admin.searchPlaceholder"),
874
+ value: search,
875
+ onChange: (e) => setSearch(e.target.value)
876
+ }
877
+ ),
878
+ /* @__PURE__ */ jsx6("span", { className: "gallery-manager__count", children: filteredItems.length })
879
+ ] });
880
+ const toolbar = /* @__PURE__ */ jsxs6("div", { className: "gallery-manager__toolbar-row", children: [
881
+ /* @__PURE__ */ jsx6("h1", { className: "gallery-manager__title", children: t4(i18n, "admin.title") }),
882
+ /* @__PURE__ */ jsx6(
883
+ "button",
884
+ {
885
+ type: "button",
886
+ className: "gallery-manager__new-btn",
887
+ onClick: handleNewClick,
888
+ children: t4(i18n, "admin.newButton")
889
+ }
890
+ )
891
+ ] });
892
+ const listRoot = /* @__PURE__ */ jsx6("ul", { className: "gallery-manager__list-ul", children: filteredItems.map((item) => /* @__PURE__ */ jsxs6(
893
+ "li",
894
+ {
895
+ className: cn("gallery-list-item", selectedId === item.id && "gallery-list-item--active"),
896
+ onClick: () => handleSelect(item.id),
897
+ children: [
898
+ /* @__PURE__ */ jsx6("img", { src: item.imageUrl, alt: "", className: "gallery-list-item__thumb" }),
899
+ /* @__PURE__ */ jsxs6("div", { className: "gallery-list-item__body", children: [
900
+ /* @__PURE__ */ jsx6("div", { className: "gallery-list-item__caption", children: item.caption || "(no caption)" }),
901
+ /* @__PURE__ */ jsxs6("div", { className: "gallery-list-item__meta", children: [
902
+ item.featured ? "\u2605 " : "",
903
+ item.published ? "\uACF5\uAC1C" : "\uBE44\uACF5\uAC1C",
904
+ " \xB7 \uC21C\uC11C ",
905
+ item.sortOrder
906
+ ] })
907
+ ] })
908
+ ]
909
+ },
910
+ item.id
911
+ )) });
912
+ const homePreview = /* @__PURE__ */ jsx6(
913
+ GalleryHomePreview,
914
+ {
915
+ items: previewItems,
916
+ maxCount: mosaicCount,
917
+ onReorder: handleReorder,
918
+ onToggleFeatured: handleToggleFeatured,
919
+ i18n
920
+ }
921
+ );
922
+ return /* @__PURE__ */ jsx6(
923
+ GalleryManagerLayout,
924
+ {
925
+ className,
926
+ listRoot,
927
+ filterControls,
928
+ toolbar,
929
+ editForm,
930
+ homePreview,
931
+ i18n,
932
+ ui
933
+ }
934
+ );
935
+ }
936
+
937
+ // src/components/CategoryAdminManager.tsx
938
+ import {
939
+ useCallback as useCallback4,
940
+ useEffect as useEffect3,
941
+ useRef as useRef5,
942
+ useState as useState4
943
+ } from "react";
944
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
945
+ var DEFAULT_TEXT6 = {
946
+ "category.title": "Gallery categories",
947
+ "category.newButton": "+ New category",
948
+ "category.slug": "Slug (uppercase)",
949
+ "category.labelKo": "Label (Ko)",
950
+ "category.labelEn": "Label (En)",
951
+ "category.deleteConfirmInUse": "This category has galleries \u2014 remove or move them first"
952
+ };
953
+ function t5(i18n, key) {
954
+ return i18n?.[key] ?? DEFAULT_TEXT6[key] ?? key;
955
+ }
956
+ var EMPTY_FORM = {
957
+ id: null,
958
+ slug: "",
959
+ labelKo: "",
960
+ labelEn: "",
961
+ sortOrder: 0,
962
+ isActive: true
963
+ };
964
+ async function clientFetch2(url, init) {
965
+ return fetch(url, {
966
+ credentials: "include",
967
+ headers: init?.body ? { "Content-Type": "application/json", ...init?.headers } : init?.headers,
968
+ ...init
969
+ });
970
+ }
971
+ async function jsonOrNull2(res) {
972
+ try {
973
+ return await res.json();
974
+ } catch {
975
+ return null;
976
+ }
977
+ }
978
+ function CategoryAdminManager(props) {
979
+ const { className } = props;
980
+ const config = getGalleryConfig();
981
+ const i18n = config.i18n ?? {};
982
+ const [items, setItems] = useState4([]);
983
+ const [form, setForm] = useState4(EMPTY_FORM);
984
+ const [error, setError] = useState4(null);
985
+ const [refreshKey, setRefreshKey] = useState4(0);
986
+ const [saving, setSaving] = useState4(false);
987
+ const mountedRef = useRef5(true);
988
+ useEffect3(() => {
989
+ mountedRef.current = true;
990
+ return () => {
991
+ mountedRef.current = false;
992
+ };
993
+ }, []);
994
+ useEffect3(() => {
995
+ let cancelled = false;
996
+ (async () => {
997
+ try {
998
+ const res = await clientFetch2("/api/admin/gallery-categories");
999
+ const json = await jsonOrNull2(res);
1000
+ if (!cancelled && mountedRef.current) {
1001
+ const data = json?.data ?? json ?? [];
1002
+ setItems(Array.isArray(data) ? data : []);
1003
+ }
1004
+ } catch {
1005
+ }
1006
+ })();
1007
+ return () => {
1008
+ cancelled = true;
1009
+ };
1010
+ }, [refreshKey]);
1011
+ const refresh = useCallback4(() => setRefreshKey((k) => k + 1), []);
1012
+ const handleEdit = useCallback4((cat) => {
1013
+ setError(null);
1014
+ setForm({
1015
+ id: cat.id,
1016
+ slug: cat.slug,
1017
+ labelKo: cat.labelKo,
1018
+ labelEn: cat.labelEn ?? "",
1019
+ sortOrder: cat.sortOrder,
1020
+ isActive: cat.isActive
1021
+ });
1022
+ }, []);
1023
+ const handleNew = useCallback4(() => {
1024
+ setError(null);
1025
+ setForm(EMPTY_FORM);
1026
+ }, []);
1027
+ const handleSave = useCallback4(async () => {
1028
+ setSaving(true);
1029
+ setError(null);
1030
+ try {
1031
+ const body = JSON.stringify({
1032
+ slug: form.slug,
1033
+ labelKo: form.labelKo,
1034
+ labelEn: form.labelEn || void 0,
1035
+ sortOrder: form.sortOrder,
1036
+ isActive: form.isActive
1037
+ });
1038
+ const url = form.id ? `/api/admin/gallery-categories/${form.id}` : "/api/admin/gallery-categories";
1039
+ const method = form.id ? "PUT" : "POST";
1040
+ const res = await clientFetch2(url, { method, body });
1041
+ if (!res.ok) {
1042
+ const json = await jsonOrNull2(res);
1043
+ setError(json?.error?.message ?? `Save failed (${res.status})`);
1044
+ return;
1045
+ }
1046
+ if (mountedRef.current) {
1047
+ setForm(EMPTY_FORM);
1048
+ refresh();
1049
+ }
1050
+ } finally {
1051
+ if (mountedRef.current) setSaving(false);
1052
+ }
1053
+ }, [form, refresh]);
1054
+ const handleDelete = useCallback4(
1055
+ async (cat) => {
1056
+ if (typeof window !== "undefined" && !window.confirm(`Delete "${cat.labelKo}"?`)) return;
1057
+ setError(null);
1058
+ const res = await clientFetch2(`/api/admin/gallery-categories/${cat.id}`, {
1059
+ method: "DELETE"
1060
+ });
1061
+ if (res.status === 409) {
1062
+ setError(t5(i18n, "category.deleteConfirmInUse"));
1063
+ return;
1064
+ }
1065
+ if (!res.ok) {
1066
+ const json = await jsonOrNull2(res);
1067
+ setError(json?.error?.message ?? `Delete failed (${res.status})`);
1068
+ return;
1069
+ }
1070
+ if (mountedRef.current) refresh();
1071
+ },
1072
+ [i18n, refresh]
1073
+ );
1074
+ return /* @__PURE__ */ jsxs7("div", { className: cn("gallery-category-admin", className), children: [
1075
+ /* @__PURE__ */ jsxs7("header", { className: "gallery-category-admin__header", children: [
1076
+ /* @__PURE__ */ jsx7("h2", { className: "gallery-category-admin__title", children: t5(i18n, "category.title") }),
1077
+ /* @__PURE__ */ jsx7(
1078
+ "button",
1079
+ {
1080
+ type: "button",
1081
+ className: "gallery-category-admin__new",
1082
+ onClick: handleNew,
1083
+ children: t5(i18n, "category.newButton")
1084
+ }
1085
+ )
1086
+ ] }),
1087
+ error ? /* @__PURE__ */ jsx7("div", { className: "gallery-category-admin__error", role: "alert", children: error }) : null,
1088
+ /* @__PURE__ */ jsx7("ul", { className: "gallery-category-admin__list", children: items.map((c) => /* @__PURE__ */ jsxs7("li", { className: "gallery-category-admin__item", children: [
1089
+ /* @__PURE__ */ jsxs7(
1090
+ "button",
1091
+ {
1092
+ type: "button",
1093
+ className: "gallery-category-admin__item-row",
1094
+ onClick: () => handleEdit(c),
1095
+ children: [
1096
+ /* @__PURE__ */ jsx7("span", { className: "gallery-category-admin__slug", children: c.slug }),
1097
+ /* @__PURE__ */ jsx7("span", { className: "gallery-category-admin__label", children: c.labelKo }),
1098
+ /* @__PURE__ */ jsxs7("span", { className: "gallery-category-admin__order", children: [
1099
+ "#",
1100
+ c.sortOrder
1101
+ ] }),
1102
+ /* @__PURE__ */ jsx7("span", { className: "gallery-category-admin__active", children: c.isActive ? "\u2713" : "\u2717" })
1103
+ ]
1104
+ }
1105
+ ),
1106
+ /* @__PURE__ */ jsx7(
1107
+ "button",
1108
+ {
1109
+ type: "button",
1110
+ className: "gallery-category-admin__delete",
1111
+ onClick: () => void handleDelete(c),
1112
+ "aria-label": "delete",
1113
+ children: "\xD7"
1114
+ }
1115
+ )
1116
+ ] }, c.id)) }),
1117
+ /* @__PURE__ */ jsxs7("div", { className: "gallery-category-admin__form", children: [
1118
+ /* @__PURE__ */ jsx7("div", { className: "gallery-category-admin__form-title", children: form.id ? "Edit category" : "New category" }),
1119
+ /* @__PURE__ */ jsxs7("div", { className: "gallery-category-admin__field", children: [
1120
+ /* @__PURE__ */ jsx7("label", { className: "gallery-category-admin__field-label", children: t5(i18n, "category.slug") }),
1121
+ /* @__PURE__ */ jsx7(
1122
+ "input",
1123
+ {
1124
+ name: "slug",
1125
+ className: "gallery-category-admin__input",
1126
+ value: form.slug,
1127
+ onChange: (e) => setForm((f) => ({ ...f, slug: e.target.value.toUpperCase() })),
1128
+ placeholder: "UPPER_SNAKE"
1129
+ }
1130
+ )
1131
+ ] }),
1132
+ /* @__PURE__ */ jsxs7("div", { className: "gallery-category-admin__field", children: [
1133
+ /* @__PURE__ */ jsx7("label", { className: "gallery-category-admin__field-label", children: t5(i18n, "category.labelKo") }),
1134
+ /* @__PURE__ */ jsx7(
1135
+ "input",
1136
+ {
1137
+ name: "labelKo",
1138
+ className: "gallery-category-admin__input",
1139
+ value: form.labelKo,
1140
+ onChange: (e) => setForm((f) => ({ ...f, labelKo: e.target.value }))
1141
+ }
1142
+ )
1143
+ ] }),
1144
+ /* @__PURE__ */ jsxs7("div", { className: "gallery-category-admin__field", children: [
1145
+ /* @__PURE__ */ jsx7("label", { className: "gallery-category-admin__field-label", children: t5(i18n, "category.labelEn") }),
1146
+ /* @__PURE__ */ jsx7(
1147
+ "input",
1148
+ {
1149
+ name: "labelEn",
1150
+ className: "gallery-category-admin__input",
1151
+ value: form.labelEn,
1152
+ onChange: (e) => setForm((f) => ({ ...f, labelEn: e.target.value }))
1153
+ }
1154
+ )
1155
+ ] }),
1156
+ /* @__PURE__ */ jsxs7("div", { className: "gallery-category-admin__field gallery-category-admin__field--row", children: [
1157
+ /* @__PURE__ */ jsx7("label", { className: "gallery-category-admin__field-label", children: "Sort order" }),
1158
+ /* @__PURE__ */ jsx7(
1159
+ "input",
1160
+ {
1161
+ type: "number",
1162
+ name: "sortOrder",
1163
+ className: "gallery-category-admin__input",
1164
+ value: form.sortOrder,
1165
+ min: 0,
1166
+ onChange: (e) => {
1167
+ const n = parseInt(e.target.value, 10);
1168
+ setForm((f) => ({ ...f, sortOrder: Number.isNaN(n) ? 0 : n }));
1169
+ }
1170
+ }
1171
+ ),
1172
+ /* @__PURE__ */ jsx7(
1173
+ ToggleSwitch,
1174
+ {
1175
+ checked: form.isActive,
1176
+ onChange: (v) => setForm((f) => ({ ...f, isActive: v })),
1177
+ label: "Active",
1178
+ size: "sm"
1179
+ }
1180
+ )
1181
+ ] }),
1182
+ /* @__PURE__ */ jsxs7("div", { className: "gallery-category-admin__form-actions", children: [
1183
+ form.id ? /* @__PURE__ */ jsx7(
1184
+ "button",
1185
+ {
1186
+ type: "button",
1187
+ className: "gallery-category-admin__cancel",
1188
+ onClick: () => setForm(EMPTY_FORM),
1189
+ children: "Cancel"
1190
+ }
1191
+ ) : null,
1192
+ /* @__PURE__ */ jsx7(
1193
+ "button",
1194
+ {
1195
+ type: "button",
1196
+ className: "gallery-category-admin__save",
1197
+ onClick: () => void handleSave(),
1198
+ disabled: saving || !form.slug || !form.labelKo,
1199
+ children: saving ? "Saving..." : form.id ? "Update" : "Create"
1200
+ }
1201
+ )
1202
+ ] })
1203
+ ] })
1204
+ ] });
1205
+ }
1206
+ export {
1207
+ CategoryAdminManager,
1208
+ GalleryAdminManager,
1209
+ GalleryEditForm,
1210
+ GalleryHomePreview,
1211
+ GalleryManagerLayout,
1212
+ ImageDropZone,
1213
+ ToggleSwitch
1214
+ };
1215
+ //# sourceMappingURL=index.mjs.map