@vinaup/media-ui 0.0.1

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/dist/index.js ADDED
@@ -0,0 +1,1032 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ MediaDetail: () => MediaDetail,
24
+ MediaGrid: () => MediaGrid,
25
+ MediaModal: () => MediaModal,
26
+ MediaUpload: () => MediaUpload,
27
+ cx: () => cx,
28
+ formatFileSize: () => formatFileSize,
29
+ validateImageFile: () => validateImageFile
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+
33
+ // src/components/MediaUpload.tsx
34
+ var import_styles = require("@mantine/dropzone/styles.css");
35
+ var import_core = require("@mantine/core");
36
+ var import_dropzone = require("@mantine/dropzone");
37
+ var import_tb = require("react-icons/tb");
38
+ var import_hi = require("react-icons/hi");
39
+ var import_react = require("react");
40
+ var import_notifications = require("@mantine/notifications");
41
+
42
+ // src/utils/helpers.ts
43
+ var validateImageFile = (file) => {
44
+ const validTypes = ["image/png", "image/jpeg", "image/jpg"];
45
+ return validTypes.includes(file.type);
46
+ };
47
+ var formatFileSize = (bytes) => {
48
+ if (bytes === 0) return "0 Bytes";
49
+ const k = 1024;
50
+ const sizes = ["Bytes", "KB", "MB"];
51
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
52
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + " " + sizes[i];
53
+ };
54
+ var cx = (...classNames) => {
55
+ const validClassNames = classNames.filter(Boolean);
56
+ const result = validClassNames.join(" ");
57
+ return result;
58
+ };
59
+
60
+ // src/components/styles/media-upload.module.scss
61
+ var media_upload_module_default = {
62
+ sectionTitleRoot: "media_upload_module_sectionTitleRoot",
63
+ copyButtonRoot: "media_upload_module_copyButtonRoot",
64
+ imageContainer: "media_upload_module_imageContainer",
65
+ itemImageRoot: "media_upload_module_itemImageRoot",
66
+ uploadingOverlay: "media_upload_module_uploadingOverlay",
67
+ statusBadge: "media_upload_module_statusBadge",
68
+ statusBadgeSuccess: "media_upload_module_statusBadgeSuccess",
69
+ statusBadgeError: "media_upload_module_statusBadgeError"
70
+ };
71
+
72
+ // src/components/MediaUpload.tsx
73
+ var import_jsx_runtime = require("react/jsx-runtime");
74
+ function MediaUpload({
75
+ onUpload,
76
+ onSave,
77
+ onUploadSuccess,
78
+ onUploadError,
79
+ maxSize = 2 * 1024 ** 2,
80
+ acceptedTypes = ["image/png", "image/jpeg", "image/jpg"],
81
+ multiple = true,
82
+ folder = "media",
83
+ classNames
84
+ }) {
85
+ const [isUploading, setIsUploading] = (0, import_react.useState)(false);
86
+ const [uploadFiles, setUploadFiles] = (0, import_react.useState)([]);
87
+ const handleDrop = async (files) => {
88
+ if (files.length === 0) return;
89
+ const invalidFiles = files.filter((file) => !validateImageFile(file));
90
+ if (invalidFiles.length > 0) {
91
+ import_notifications.notifications.show({
92
+ title: "Invalid file type",
93
+ message: `${invalidFiles.length} file(s) have invalid type.`,
94
+ color: "red"
95
+ });
96
+ return;
97
+ }
98
+ setIsUploading(true);
99
+ const newUploadFiles = files.map((file, index) => ({
100
+ id: `${Date.now()}-${index}`,
101
+ file,
102
+ preview: URL.createObjectURL(file),
103
+ status: "uploading"
104
+ }));
105
+ setUploadFiles((prev) => [...newUploadFiles, ...prev]);
106
+ try {
107
+ const uploadResults = await onUpload(files);
108
+ setUploadFiles(
109
+ (prev) => prev.map((item) => {
110
+ const idx = newUploadFiles.findIndex((f) => f.id === item.id);
111
+ if (idx !== -1 && uploadResults[idx]) {
112
+ return { ...item, status: "success", url: uploadResults[idx].url };
113
+ }
114
+ return item;
115
+ })
116
+ );
117
+ if (onSave) {
118
+ const mediaData = uploadResults.map((result) => ({
119
+ name: result.name,
120
+ title: null,
121
+ description: null,
122
+ url: result.url,
123
+ type: "image",
124
+ folder
125
+ }));
126
+ const savedMedia = await onSave(mediaData);
127
+ onUploadSuccess?.(savedMedia);
128
+ }
129
+ } catch (error) {
130
+ setUploadFiles(
131
+ (prev) => prev.map(
132
+ (item) => newUploadFiles.some((f) => f.id === item.id) ? { ...item, status: "error", error: error.message } : item
133
+ )
134
+ );
135
+ onUploadError?.(error);
136
+ } finally {
137
+ setIsUploading(false);
138
+ }
139
+ };
140
+ const handleReject = (files) => {
141
+ import_notifications.notifications.show({
142
+ title: "Files rejected",
143
+ message: `${files.length} file(s) were rejected. Please check file type and size (\u2264 2MB).`,
144
+ color: "red"
145
+ });
146
+ };
147
+ (0, import_react.useEffect)(() => {
148
+ return () => {
149
+ uploadFiles.forEach((item) => {
150
+ if (item.preview.startsWith("blob:")) {
151
+ URL.revokeObjectURL(item.preview);
152
+ }
153
+ });
154
+ };
155
+ }, [uploadFiles]);
156
+ const handleCopyLink = async (url) => {
157
+ try {
158
+ await navigator.clipboard.writeText(url);
159
+ import_notifications.notifications.show({
160
+ title: "Link copied",
161
+ message: "Image URL has been copied to clipboard",
162
+ color: "green"
163
+ });
164
+ } catch (error) {
165
+ import_notifications.notifications.show({
166
+ title: "Copy failed",
167
+ message: error instanceof Error ? error.message : "Failed to copy link to clipboard",
168
+ color: "red"
169
+ });
170
+ }
171
+ };
172
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
173
+ import_core.Stack,
174
+ {
175
+ gap: "lg",
176
+ classNames: {
177
+ root: classNames?.rootStack?.root
178
+ },
179
+ children: [
180
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
181
+ import_dropzone.Dropzone,
182
+ {
183
+ onDrop: handleDrop,
184
+ onReject: handleReject,
185
+ maxSize,
186
+ accept: acceptedTypes,
187
+ disabled: isUploading,
188
+ multiple,
189
+ classNames: classNames?.dropzone,
190
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
191
+ import_core.Group,
192
+ {
193
+ justify: "center",
194
+ gap: "xl",
195
+ mih: 220,
196
+ classNames: {
197
+ root: classNames?.dropzoneGroup?.root
198
+ },
199
+ children: [
200
+ isUploading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_core.Loader, { size: 52 }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
201
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dropzone.DropzoneAccept, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tb.TbUpload, { size: 52, color: "blue" }) }),
202
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dropzone.DropzoneReject, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_hi.HiOutlineX, { size: 52, color: "red" }) }),
203
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dropzone.DropzoneIdle, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tb.TbPhoto, { size: 52 }) })
204
+ ] }),
205
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
206
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
207
+ import_core.Text,
208
+ {
209
+ size: "xl",
210
+ inline: true,
211
+ classNames: {
212
+ root: classNames?.dropzoneText?.root
213
+ },
214
+ children: "Drag images here or click to select files"
215
+ }
216
+ ),
217
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
218
+ import_core.Text,
219
+ {
220
+ size: "sm",
221
+ c: "dimmed",
222
+ inline: true,
223
+ mt: 7,
224
+ classNames: {
225
+ root: classNames?.dropzoneSubtext?.root
226
+ },
227
+ children: "(png, jpg, jpeg; Size \u2264 2M)"
228
+ }
229
+ )
230
+ ] })
231
+ ]
232
+ }
233
+ )
234
+ }
235
+ ),
236
+ uploadFiles.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
237
+ import_core.Stack,
238
+ {
239
+ gap: "xs",
240
+ classNames: {
241
+ root: classNames?.recentStack?.root
242
+ },
243
+ children: [
244
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
245
+ import_core.Text,
246
+ {
247
+ fw: 500,
248
+ classNames: {
249
+ root: cx(media_upload_module_default.sectionTitleRoot, classNames?.sectionTitle?.root)
250
+ },
251
+ children: "Recently Uploaded Images"
252
+ }
253
+ ),
254
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_core.Grid, { classNames: classNames?.grid, children: uploadFiles.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_core.GridCol, { span: { base: 6, sm: 6, md: 3, lg: 2 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
255
+ import_core.Paper,
256
+ {
257
+ p: "sm",
258
+ withBorder: true,
259
+ radius: "md",
260
+ classNames: {
261
+ root: classNames?.itemPaper?.root
262
+ },
263
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
264
+ import_core.Stack,
265
+ {
266
+ gap: 6,
267
+ classNames: {
268
+ root: classNames?.itemStack?.root
269
+ },
270
+ children: [
271
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cx(media_upload_module_default.imageContainer, classNames?.imageContainer), children: [
272
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
273
+ import_core.Image,
274
+ {
275
+ src: item.preview,
276
+ alt: item.file.name,
277
+ fit: "cover",
278
+ classNames: {
279
+ root: cx(media_upload_module_default.itemImageRoot, classNames?.itemImage?.root)
280
+ }
281
+ }
282
+ ),
283
+ item.status === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cx(media_upload_module_default.uploadingOverlay, classNames?.uploadingOverlay), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_core.Loader, { size: "sm", color: "white" }) }),
284
+ item.status === "success" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cx(
285
+ media_upload_module_default.statusBadge,
286
+ media_upload_module_default.statusBadgeSuccess,
287
+ classNames?.statusBadge,
288
+ classNames?.statusBadgeSuccess
289
+ ), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_core.Text, { size: "xs", c: "white", fw: 700, children: "\u2713" }) }),
290
+ item.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cx(
291
+ media_upload_module_default.statusBadge,
292
+ media_upload_module_default.statusBadgeError,
293
+ classNames?.statusBadge,
294
+ classNames?.statusBadgeError
295
+ ), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_hi.HiOutlineX, { size: 16, color: "white" }) })
296
+ ] }),
297
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
298
+ import_core.Text,
299
+ {
300
+ size: "xs",
301
+ c: "dimmed",
302
+ lineClamp: 1,
303
+ title: item.file.name,
304
+ classNames: {
305
+ root: classNames?.itemFilename?.root
306
+ },
307
+ children: item.file.name
308
+ }
309
+ ),
310
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
311
+ import_core.Group,
312
+ {
313
+ justify: "space-between",
314
+ align: "end",
315
+ gap: "xs",
316
+ classNames: {
317
+ root: classNames?.itemGroup?.root
318
+ },
319
+ children: [
320
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
321
+ import_core.Text,
322
+ {
323
+ size: "xs",
324
+ c: "dimmed",
325
+ classNames: {
326
+ root: classNames?.itemFilesize?.root
327
+ },
328
+ children: formatFileSize(item.file.size)
329
+ }
330
+ ),
331
+ item.status === "success" && item.url && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
332
+ import_core.UnstyledButton,
333
+ {
334
+ onClick: () => handleCopyLink(item.url),
335
+ classNames: {
336
+ root: cx(media_upload_module_default.copyButtonRoot, classNames?.copyButton?.root)
337
+ },
338
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
339
+ import_core.Text,
340
+ {
341
+ size: "sm",
342
+ c: "blue",
343
+ classNames: {
344
+ root: classNames?.copyButtonText?.root
345
+ },
346
+ children: "Copy link"
347
+ }
348
+ )
349
+ }
350
+ )
351
+ ]
352
+ }
353
+ ),
354
+ item.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
355
+ import_core.Text,
356
+ {
357
+ size: "xs",
358
+ c: "red",
359
+ classNames: {
360
+ root: classNames?.itemError?.root
361
+ },
362
+ children: item.error || "Upload failed"
363
+ }
364
+ )
365
+ ]
366
+ }
367
+ )
368
+ }
369
+ ) }, item.id)) })
370
+ ]
371
+ }
372
+ )
373
+ ]
374
+ }
375
+ );
376
+ }
377
+
378
+ // src/components/MediaGrid.tsx
379
+ var import_core2 = require("@mantine/core");
380
+ var import_react2 = require("react");
381
+ var import_io5 = require("react-icons/io5");
382
+
383
+ // src/components/styles/media-grid.module.scss
384
+ var media_grid_module_default = {
385
+ itemPaperRoot: "media_grid_module_itemPaperRoot",
386
+ selectedPaper: "media_grid_module_selectedPaper",
387
+ imageContainer: "media_grid_module_imageContainer",
388
+ itemImageRoot: "media_grid_module_itemImageRoot",
389
+ itemTextRoot: "media_grid_module_itemTextRoot"
390
+ };
391
+
392
+ // src/components/MediaGrid.tsx
393
+ var import_jsx_runtime2 = require("react/jsx-runtime");
394
+ function MediaGrid({
395
+ images,
396
+ selectedImageId = null,
397
+ onImageClick,
398
+ sortOptions = [
399
+ { value: "createdAt", label: "By created date" },
400
+ { value: "updatedAt", label: "By updated date" },
401
+ { value: "title", label: "By title" }
402
+ ],
403
+ classNames
404
+ }) {
405
+ const [searchQuery, setSearchQuery] = (0, import_react2.useState)("");
406
+ const [sortBy, setSortBy] = (0, import_react2.useState)("createdAt");
407
+ const filteredAndSortedImages = () => {
408
+ let result = [...images];
409
+ if (searchQuery.trim()) {
410
+ const query = searchQuery.toLowerCase();
411
+ result = result.filter(
412
+ (image) => image.title?.toLowerCase().includes(query)
413
+ );
414
+ }
415
+ if (sortBy) {
416
+ result.sort((a, b) => {
417
+ switch (sortBy) {
418
+ case "createdAt":
419
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
420
+ case "updatedAt":
421
+ return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
422
+ case "title":
423
+ return (a.title ?? "").localeCompare(b.title ?? "");
424
+ default:
425
+ return 0;
426
+ }
427
+ });
428
+ }
429
+ return result;
430
+ };
431
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
432
+ import_core2.Stack,
433
+ {
434
+ gap: "md",
435
+ classNames: {
436
+ root: classNames?.rootStack?.root
437
+ },
438
+ children: [
439
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
440
+ import_core2.Group,
441
+ {
442
+ justify: "space-between",
443
+ align: "center",
444
+ classNames: {
445
+ root: classNames?.filterGroup?.root
446
+ },
447
+ children: [
448
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
449
+ import_core2.Select,
450
+ {
451
+ placeholder: "Sort by",
452
+ value: sortBy,
453
+ onChange: setSortBy,
454
+ data: sortOptions,
455
+ w: 200,
456
+ clearable: false,
457
+ classNames: classNames?.sortSelect
458
+ }
459
+ ),
460
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
461
+ import_core2.TextInput,
462
+ {
463
+ placeholder: "Search by title...",
464
+ value: searchQuery,
465
+ onChange: (e) => setSearchQuery(e.currentTarget.value),
466
+ leftSection: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_io5.IoSearch, { size: 16 }),
467
+ w: 250,
468
+ classNames: classNames?.searchInput
469
+ }
470
+ )
471
+ ]
472
+ }
473
+ ),
474
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
475
+ import_core2.Grid,
476
+ {
477
+ gutter: "md",
478
+ classNames: classNames?.grid,
479
+ children: filteredAndSortedImages().map((image) => {
480
+ const isSelected = selectedImageId === image.id;
481
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_core2.GridCol, { span: { base: 6, sm: 4, md: 3, lg: 2 }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
482
+ import_core2.Paper,
483
+ {
484
+ p: "xs",
485
+ withBorder: true,
486
+ radius: "md",
487
+ onClick: () => onImageClick(image.id),
488
+ classNames: {
489
+ root: cx(
490
+ media_grid_module_default.itemPaperRoot,
491
+ classNames?.itemPaper?.root,
492
+ isSelected ? media_grid_module_default.selectedPaper : void 0,
493
+ isSelected ? classNames?.selectedPaper : void 0
494
+ )
495
+ },
496
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
497
+ import_core2.Stack,
498
+ {
499
+ gap: 6,
500
+ classNames: {
501
+ root: classNames?.itemStack?.root
502
+ },
503
+ children: [
504
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: cx(media_grid_module_default.imageContainer, classNames?.imageContainer), children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
505
+ import_core2.Image,
506
+ {
507
+ src: image.url,
508
+ alt: image.title || image.name,
509
+ fit: "cover",
510
+ classNames: {
511
+ root: cx(media_grid_module_default.itemImageRoot, classNames?.itemImage?.root)
512
+ }
513
+ }
514
+ ) }),
515
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
516
+ import_core2.Text,
517
+ {
518
+ size: "xs",
519
+ c: "dimmed",
520
+ lineClamp: 1,
521
+ title: image.title ?? image.name,
522
+ classNames: {
523
+ root: cx(media_grid_module_default.itemTextRoot, classNames?.itemText?.root)
524
+ },
525
+ children: image.title ?? image.name
526
+ }
527
+ )
528
+ ]
529
+ }
530
+ )
531
+ }
532
+ ) }, image.id);
533
+ })
534
+ }
535
+ )
536
+ ]
537
+ }
538
+ );
539
+ }
540
+
541
+ // src/components/MediaDetail.tsx
542
+ var import_core3 = require("@mantine/core");
543
+ var import_react3 = require("react");
544
+ var import_md = require("react-icons/md");
545
+ var import_gr = require("react-icons/gr");
546
+
547
+ // src/components/styles/media-detail.module.scss
548
+ var media_detail_module_default = {
549
+ paperRoot: "media_detail_module_paperRoot",
550
+ imageWrapper: "media_detail_module_imageWrapper",
551
+ urlText: "media_detail_module_urlText",
552
+ deleteButtonRoot: "media_detail_module_deleteButtonRoot"
553
+ };
554
+
555
+ // src/components/MediaDetail.tsx
556
+ var import_bs = require("react-icons/bs");
557
+ var import_jsx_runtime3 = require("react/jsx-runtime");
558
+ function MediaDetail({
559
+ image,
560
+ onUpdate,
561
+ onDelete,
562
+ onCopyLink,
563
+ onNotify,
564
+ classNames
565
+ }) {
566
+ const [title, setTitle] = (0, import_react3.useState)(image.title || "");
567
+ const [description, setDescription] = (0, import_react3.useState)(image.description || "");
568
+ const [originalTitle, setOriginalTitle] = (0, import_react3.useState)(image.title || "");
569
+ const [originalDescription, setOriginalDescription] = (0, import_react3.useState)(image.description || "");
570
+ const isTitleDirty = title !== originalTitle;
571
+ const isDescriptionDirty = description !== originalDescription;
572
+ const [isSavingTitle, setIsSavingTitle] = (0, import_react3.useState)(false);
573
+ const [isSavingDescription, setIsSavingDescription] = (0, import_react3.useState)(false);
574
+ const handleSaveTitle = async () => {
575
+ setIsSavingTitle(true);
576
+ try {
577
+ await onUpdate(image.id, { title: title || null });
578
+ setOriginalTitle(title);
579
+ onNotify?.("success", "Title saved");
580
+ } catch (error) {
581
+ onNotify?.("error", error.message);
582
+ } finally {
583
+ setIsSavingTitle(false);
584
+ }
585
+ };
586
+ const handleSaveDescription = async () => {
587
+ setIsSavingDescription(true);
588
+ try {
589
+ await onUpdate(image.id, { description: description || null });
590
+ setOriginalDescription(description);
591
+ onNotify?.("success", "Description saved");
592
+ } catch (error) {
593
+ onNotify?.("error", error.message);
594
+ } finally {
595
+ setIsSavingDescription(false);
596
+ }
597
+ };
598
+ const handleCopyLink = async () => {
599
+ if (onCopyLink) {
600
+ onCopyLink(image.url);
601
+ } else {
602
+ try {
603
+ await navigator.clipboard.writeText(image.url);
604
+ onNotify?.("success", "Link copied");
605
+ } catch {
606
+ onNotify?.("error", "Failed to copy link");
607
+ }
608
+ }
609
+ };
610
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
611
+ import_core3.Paper,
612
+ {
613
+ p: "md",
614
+ radius: "md",
615
+ withBorder: true,
616
+ classNames: {
617
+ root: cx(media_detail_module_default.paperRoot, classNames?.paper?.root)
618
+ },
619
+ children: [
620
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_core3.Stack, { gap: "md", children: [
621
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
622
+ import_core3.Title,
623
+ {
624
+ order: 4,
625
+ classNames: {
626
+ root: cx(media_detail_module_default.titleRoot, classNames?.title?.root)
627
+ },
628
+ children: "Image Details"
629
+ }
630
+ ),
631
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: cx(media_detail_module_default.imageWrapper, classNames?.imageWrapper), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
632
+ import_core3.Image,
633
+ {
634
+ src: image.url,
635
+ alt: image.title || image.name,
636
+ fit: "contain",
637
+ radius: "md",
638
+ classNames: {
639
+ root: cx(media_detail_module_default.imageRoot, classNames?.image?.root)
640
+ }
641
+ }
642
+ ) }),
643
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
644
+ import_core3.Stack,
645
+ {
646
+ gap: "xs",
647
+ classNames: {
648
+ root: cx(media_detail_module_default.fieldsStackRoot, classNames?.fieldsStack?.root)
649
+ },
650
+ children: [
651
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cx(media_detail_module_default.fieldGroup, classNames?.fieldGroup), children: [
652
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
653
+ import_core3.Text,
654
+ {
655
+ size: "sm",
656
+ fw: 500,
657
+ c: "dimmed",
658
+ classNames: {
659
+ root: cx(media_detail_module_default.fieldLabelRoot, classNames?.fieldLabel?.root)
660
+ },
661
+ children: "Name"
662
+ }
663
+ ),
664
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
665
+ import_core3.Text,
666
+ {
667
+ classNames: {
668
+ root: cx(media_detail_module_default.fieldValueRoot, classNames?.fieldValue?.root)
669
+ },
670
+ children: image.name
671
+ }
672
+ )
673
+ ] }),
674
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cx(media_detail_module_default.fieldGroup, classNames?.fieldGroup), children: [
675
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_core3.Group, { gap: 4, align: "center", children: [
676
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
677
+ import_core3.Text,
678
+ {
679
+ size: "sm",
680
+ fw: 500,
681
+ c: "dimmed",
682
+ classNames: { root: cx(media_detail_module_default.fieldLabelRoot, classNames?.fieldLabel?.root) },
683
+ children: "Title"
684
+ }
685
+ ),
686
+ isTitleDirty && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
687
+ import_core3.ActionIcon,
688
+ {
689
+ size: "xs",
690
+ variant: "transparent",
691
+ color: "green",
692
+ onClick: handleSaveTitle,
693
+ loading: isSavingTitle,
694
+ title: "Save title",
695
+ classNames: {
696
+ root: cx(media_detail_module_default.saveButtonRoot, classNames?.saveButton?.root)
697
+ },
698
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_bs.BsSaveFill, { size: 12 })
699
+ }
700
+ )
701
+ ] }),
702
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
703
+ import_core3.Textarea,
704
+ {
705
+ value: title,
706
+ placeholder: "Enter title",
707
+ onChange: (e) => setTitle(e.target.value),
708
+ onKeyDown: (e) => {
709
+ if (e.key === "Enter" && !e.shiftKey) {
710
+ e.preventDefault();
711
+ if (isTitleDirty) handleSaveTitle();
712
+ }
713
+ },
714
+ minRows: 2,
715
+ autosize: true,
716
+ classNames: {
717
+ root: cx(media_detail_module_default.textareaRoot, classNames?.textarea?.root),
718
+ input: cx(media_detail_module_default.textareaInput, classNames?.textarea?.input),
719
+ wrapper: cx(media_detail_module_default.textareaWrapper, classNames?.textarea?.wrapper)
720
+ }
721
+ }
722
+ )
723
+ ] }),
724
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cx(media_detail_module_default.fieldGroup, classNames?.fieldGroup), children: [
725
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_core3.Group, { gap: 4, align: "center", children: [
726
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
727
+ import_core3.Text,
728
+ {
729
+ size: "sm",
730
+ fw: 500,
731
+ c: "dimmed",
732
+ classNames: { root: cx(media_detail_module_default.fieldLabelRoot, classNames?.fieldLabel?.root) },
733
+ children: "Description"
734
+ }
735
+ ),
736
+ isDescriptionDirty && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
737
+ import_core3.ActionIcon,
738
+ {
739
+ size: "xs",
740
+ variant: "transparent",
741
+ color: "green",
742
+ onClick: handleSaveDescription,
743
+ loading: isSavingDescription,
744
+ title: "Save description",
745
+ classNames: {
746
+ root: cx(media_detail_module_default.saveButtonRoot, classNames?.saveButton?.root)
747
+ },
748
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_bs.BsSaveFill, { size: 12 })
749
+ }
750
+ )
751
+ ] }),
752
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
753
+ import_core3.Textarea,
754
+ {
755
+ value: description,
756
+ placeholder: "Enter description",
757
+ onChange: (e) => setDescription(e.target.value),
758
+ onKeyDown: (e) => {
759
+ if (e.key === "Enter" && !e.shiftKey) {
760
+ e.preventDefault();
761
+ if (isDescriptionDirty) handleSaveDescription();
762
+ }
763
+ },
764
+ minRows: 3,
765
+ autosize: true,
766
+ classNames: {
767
+ root: cx(media_detail_module_default.textareaRoot, classNames?.textarea?.root),
768
+ input: cx(media_detail_module_default.textareaInput, classNames?.textarea?.input),
769
+ wrapper: cx(media_detail_module_default.textareaWrapper, classNames?.textarea?.wrapper)
770
+ }
771
+ }
772
+ )
773
+ ] }),
774
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cx(media_detail_module_default.fieldGroup, classNames?.fieldGroup), children: [
775
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
776
+ import_core3.Text,
777
+ {
778
+ size: "sm",
779
+ fw: 500,
780
+ c: "dimmed",
781
+ classNames: {
782
+ root: cx(media_detail_module_default.fieldLabelRoot, classNames?.fieldLabel?.root)
783
+ },
784
+ children: "Type"
785
+ }
786
+ ),
787
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
788
+ import_core3.Text,
789
+ {
790
+ classNames: {
791
+ root: cx(media_detail_module_default.fieldValueRoot, classNames?.fieldValue?.root)
792
+ },
793
+ children: image.type
794
+ }
795
+ )
796
+ ] }),
797
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cx(media_detail_module_default.fieldGroup, classNames?.fieldGroup), children: [
798
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
799
+ import_core3.Text,
800
+ {
801
+ size: "sm",
802
+ fw: 500,
803
+ c: "dimmed",
804
+ classNames: {
805
+ root: cx(media_detail_module_default.fieldLabelRoot, classNames?.fieldLabel?.root)
806
+ },
807
+ children: "URL"
808
+ }
809
+ ),
810
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_core3.Group, { gap: "xs", align: "center", wrap: "nowrap", children: [
811
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
812
+ import_core3.Text,
813
+ {
814
+ size: "xs",
815
+ c: "dimmed",
816
+ classNames: {
817
+ root: cx(media_detail_module_default.urlText, classNames?.urlText)
818
+ },
819
+ children: image.url
820
+ }
821
+ ),
822
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
823
+ import_core3.ActionIcon,
824
+ {
825
+ variant: "subtle",
826
+ color: "gray",
827
+ onClick: handleCopyLink,
828
+ size: "sm",
829
+ classNames: {
830
+ root: cx(media_detail_module_default.copyButtonRoot, classNames?.copyButton?.root)
831
+ },
832
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_md.MdOutlineFileCopy, { size: 18 })
833
+ }
834
+ )
835
+ ] })
836
+ ] }),
837
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cx(media_detail_module_default.fieldGroup, classNames?.fieldGroup), children: [
838
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
839
+ import_core3.Text,
840
+ {
841
+ size: "sm",
842
+ fw: 500,
843
+ c: "dimmed",
844
+ classNames: {
845
+ root: cx(media_detail_module_default.fieldLabelRoot, classNames?.fieldLabel?.root)
846
+ },
847
+ children: "Created At"
848
+ }
849
+ ),
850
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
851
+ import_core3.Text,
852
+ {
853
+ size: "sm",
854
+ classNames: {
855
+ root: cx(media_detail_module_default.fieldValueRoot, classNames?.fieldValue?.root)
856
+ },
857
+ children: new Date(image.createdAt).toLocaleString()
858
+ }
859
+ )
860
+ ] }),
861
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cx(media_detail_module_default.fieldGroup, classNames?.fieldGroup), children: [
862
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
863
+ import_core3.Text,
864
+ {
865
+ size: "sm",
866
+ fw: 500,
867
+ c: "dimmed",
868
+ classNames: {
869
+ root: cx(media_detail_module_default.fieldLabelRoot, classNames?.fieldLabel?.root)
870
+ },
871
+ children: "Updated At"
872
+ }
873
+ ),
874
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
875
+ import_core3.Text,
876
+ {
877
+ size: "sm",
878
+ classNames: {
879
+ root: cx(media_detail_module_default.fieldValueRoot, classNames?.fieldValue?.root)
880
+ },
881
+ children: new Date(image.updatedAt).toLocaleString()
882
+ }
883
+ )
884
+ ] })
885
+ ]
886
+ }
887
+ )
888
+ ] }),
889
+ onDelete && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
890
+ import_core3.ActionIcon,
891
+ {
892
+ variant: "filled",
893
+ color: "red",
894
+ size: "lg",
895
+ onClick: () => onDelete(image.id),
896
+ classNames: {
897
+ root: cx(media_detail_module_default.deleteButtonRoot, classNames?.deleteButton?.root)
898
+ },
899
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_gr.GrTrash, { size: 20 })
900
+ }
901
+ )
902
+ ]
903
+ }
904
+ ) });
905
+ }
906
+
907
+ // src/components/MediaModal.tsx
908
+ var import_core4 = require("@mantine/core");
909
+ var import_react4 = require("react");
910
+
911
+ // src/components/styles/media-modal.module.scss
912
+ var media_modal_module_default = {
913
+ footer: "media_modal_module_footer",
914
+ tabPanel: "media_modal_module_tabPanel"
915
+ };
916
+
917
+ // src/components/MediaModal.tsx
918
+ var import_jsx_runtime4 = require("react/jsx-runtime");
919
+ function MediaModal({
920
+ opened,
921
+ onClose,
922
+ images,
923
+ onSelect,
924
+ title = "Media Library",
925
+ submitLabel = "Select",
926
+ cancelLabel = "Cancel",
927
+ onUpload,
928
+ onSave,
929
+ onUploadSuccess,
930
+ onUploadError,
931
+ classNames
932
+ }) {
933
+ const [activeTab, setActiveTab] = (0, import_react4.useState)("library");
934
+ const [selectedImage, setSelectedImage] = (0, import_react4.useState)(null);
935
+ (0, import_react4.useEffect)(() => {
936
+ if (opened) {
937
+ setSelectedImage(null);
938
+ setActiveTab("library");
939
+ }
940
+ }, [opened]);
941
+ const handleImageClick = (id) => {
942
+ const image = images.find((img) => img.id === id);
943
+ if (image) {
944
+ setSelectedImage(image);
945
+ }
946
+ };
947
+ const handleSubmit = () => {
948
+ if (selectedImage) {
949
+ onSelect(selectedImage);
950
+ onClose();
951
+ }
952
+ };
953
+ const handleInternalUploadSuccess = (media) => {
954
+ if (onUploadSuccess) {
955
+ onUploadSuccess(media);
956
+ }
957
+ setActiveTab("library");
958
+ if (media.length > 0) {
959
+ setSelectedImage(media[0]);
960
+ }
961
+ };
962
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
963
+ import_core4.Modal,
964
+ {
965
+ opened,
966
+ onClose,
967
+ title,
968
+ size: "xl",
969
+ classNames: classNames?.modal,
970
+ children: [
971
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
972
+ import_core4.Tabs,
973
+ {
974
+ value: activeTab,
975
+ onChange: setActiveTab,
976
+ classNames: classNames?.tabs,
977
+ children: [
978
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_core4.Tabs.List, { children: [
979
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_core4.Tabs.Tab, { value: "library", children: "Library" }),
980
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_core4.Tabs.Tab, { value: "upload", children: "Upload" })
981
+ ] }),
982
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_core4.Tabs.Panel, { value: "library", className: cx(media_modal_module_default.tabPanel, classNames?.tabs?.panel), children: images.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_core4.Stack, { align: "center", justify: "center", h: 300, children: [
983
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_core4.Text, { c: "dimmed", children: "No images found" }),
984
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_core4.Button, { variant: "light", onClick: () => setActiveTab("upload"), children: "Upload new image" })
985
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
986
+ MediaGrid,
987
+ {
988
+ images,
989
+ selectedImageId: selectedImage?.id,
990
+ onImageClick: handleImageClick
991
+ }
992
+ ) }),
993
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_core4.Tabs.Panel, { value: "upload", className: cx(media_modal_module_default.tabPanel, classNames?.tabs?.panel), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
994
+ MediaUpload,
995
+ {
996
+ onUpload,
997
+ onSave,
998
+ onUploadSuccess: handleInternalUploadSuccess,
999
+ onUploadError,
1000
+ folder: "media",
1001
+ multiple: true
1002
+ }
1003
+ ) })
1004
+ ]
1005
+ }
1006
+ ),
1007
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: cx(media_modal_module_default.footer, classNames?.footer?.root), children: [
1008
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_core4.Button, { variant: "default", onClick: onClose, children: cancelLabel }),
1009
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1010
+ import_core4.Button,
1011
+ {
1012
+ onClick: handleSubmit,
1013
+ disabled: !selectedImage,
1014
+ children: submitLabel
1015
+ }
1016
+ )
1017
+ ] })
1018
+ ]
1019
+ }
1020
+ );
1021
+ }
1022
+ // Annotate the CommonJS export names for ESM import in node:
1023
+ 0 && (module.exports = {
1024
+ MediaDetail,
1025
+ MediaGrid,
1026
+ MediaModal,
1027
+ MediaUpload,
1028
+ cx,
1029
+ formatFileSize,
1030
+ validateImageFile
1031
+ });
1032
+ //# sourceMappingURL=index.js.map