gavaengine 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/dist/chunk-CE7E5MAB.js +2716 -0
- package/dist/chunk-CE7E5MAB.js.map +1 -0
- package/dist/chunk-GGD7I4JO.js +253 -0
- package/dist/chunk-GGD7I4JO.js.map +1 -0
- package/dist/chunk-KCQJHXZP.js +67 -0
- package/dist/chunk-KCQJHXZP.js.map +1 -0
- package/dist/cli/index.js +242 -0
- package/dist/components/index.d.ts +154 -0
- package/dist/components/index.js +47 -0
- package/dist/components/index.js.map +1 -0
- package/dist/handlers/index.d.ts +253 -0
- package/dist/handlers/index.js +587 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/index-CIX0MBHm.d.ts +180 -0
- package/dist/index.d.ts +130 -0
- package/dist/index.js +495 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/index.d.ts +4 -0
- package/dist/providers/index.js +28 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/styles/gavaengine.css +733 -0
- package/dist/types-BZgSeTU8.d.ts +101 -0
- package/package.json +103 -0
- package/src/prisma/schema.partial.prisma +76 -0
|
@@ -0,0 +1,2716 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
useGavaActions,
|
|
4
|
+
useGavaConfig,
|
|
5
|
+
useSplash
|
|
6
|
+
} from "./chunk-GGD7I4JO.js";
|
|
7
|
+
|
|
8
|
+
// src/components/editor/ArticleEditor.tsx
|
|
9
|
+
import { useState as useState6, useEffect as useEffect4, useCallback as useCallback4, useRef as useRef3 } from "react";
|
|
10
|
+
import { useRouter } from "next/navigation";
|
|
11
|
+
import { useEditor, EditorContent } from "@tiptap/react";
|
|
12
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
13
|
+
|
|
14
|
+
// src/components/editor/extensions/ResizableImageExtension.ts
|
|
15
|
+
import { Node, mergeAttributes } from "@tiptap/core";
|
|
16
|
+
import { ReactNodeViewRenderer } from "@tiptap/react";
|
|
17
|
+
|
|
18
|
+
// src/components/editor/extensions/ResizableImageView.tsx
|
|
19
|
+
import { NodeViewWrapper } from "@tiptap/react";
|
|
20
|
+
import { useCallback, useRef, useState } from "react";
|
|
21
|
+
import { AlignLeft, AlignCenter, AlignRight, Trash2 } from "lucide-react";
|
|
22
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
23
|
+
function ResizableImageView({
|
|
24
|
+
node,
|
|
25
|
+
updateAttributes,
|
|
26
|
+
selected,
|
|
27
|
+
deleteNode
|
|
28
|
+
}) {
|
|
29
|
+
const { src, alt, width, textWrap } = node.attrs;
|
|
30
|
+
const wrapperRef = useRef(null);
|
|
31
|
+
const [resizing, setResizing] = useState(false);
|
|
32
|
+
const onResizeStart = useCallback(
|
|
33
|
+
(e, handle) => {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
e.stopPropagation();
|
|
36
|
+
setResizing(true);
|
|
37
|
+
const startX = e.clientX;
|
|
38
|
+
const container = wrapperRef.current?.closest(
|
|
39
|
+
".article-content, .tiptap"
|
|
40
|
+
);
|
|
41
|
+
if (!container) return;
|
|
42
|
+
const containerWidth = container.getBoundingClientRect().width;
|
|
43
|
+
const startWidthPx = wrapperRef.current.getBoundingClientRect().width;
|
|
44
|
+
const isLeft = handle === "nw" || handle === "sw";
|
|
45
|
+
const onMove = (ev) => {
|
|
46
|
+
const rawDelta = ev.clientX - startX;
|
|
47
|
+
const delta = isLeft ? -rawDelta : rawDelta;
|
|
48
|
+
const newPx = Math.max(startWidthPx + delta, containerWidth * 0.1);
|
|
49
|
+
const pct = Math.min(newPx / containerWidth * 100, 100);
|
|
50
|
+
updateAttributes({ width: `${Math.round(pct)}%` });
|
|
51
|
+
};
|
|
52
|
+
const onUp = () => {
|
|
53
|
+
setResizing(false);
|
|
54
|
+
document.removeEventListener("mousemove", onMove);
|
|
55
|
+
document.removeEventListener("mouseup", onUp);
|
|
56
|
+
};
|
|
57
|
+
document.addEventListener("mousemove", onMove);
|
|
58
|
+
document.addEventListener("mouseup", onUp);
|
|
59
|
+
},
|
|
60
|
+
[updateAttributes]
|
|
61
|
+
);
|
|
62
|
+
const setWrap = useCallback(
|
|
63
|
+
(mode) => {
|
|
64
|
+
if (mode === "none") {
|
|
65
|
+
updateAttributes({ textWrap: "none", width: "100%" });
|
|
66
|
+
} else {
|
|
67
|
+
const pct = parseInt(width, 10);
|
|
68
|
+
updateAttributes({
|
|
69
|
+
textWrap: mode,
|
|
70
|
+
width: pct > 60 ? "40%" : width
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
[updateAttributes, width]
|
|
75
|
+
);
|
|
76
|
+
return /* @__PURE__ */ jsxs(
|
|
77
|
+
NodeViewWrapper,
|
|
78
|
+
{
|
|
79
|
+
className: ["image-node", selected && "selected", resizing && "resizing"].filter(Boolean).join(" "),
|
|
80
|
+
"data-text-wrap": textWrap,
|
|
81
|
+
style: { width },
|
|
82
|
+
ref: wrapperRef,
|
|
83
|
+
children: [
|
|
84
|
+
/* @__PURE__ */ jsx(
|
|
85
|
+
"img",
|
|
86
|
+
{
|
|
87
|
+
src,
|
|
88
|
+
alt: alt || "",
|
|
89
|
+
draggable: false,
|
|
90
|
+
"data-drag-handle": true,
|
|
91
|
+
className: "image-node__img"
|
|
92
|
+
}
|
|
93
|
+
),
|
|
94
|
+
selected && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
95
|
+
["nw", "ne", "sw", "se"].map((h) => /* @__PURE__ */ jsx(
|
|
96
|
+
"span",
|
|
97
|
+
{
|
|
98
|
+
className: `image-node__handle image-node__handle--${h}`,
|
|
99
|
+
onMouseDown: (e) => onResizeStart(e, h),
|
|
100
|
+
contentEditable: false
|
|
101
|
+
},
|
|
102
|
+
h
|
|
103
|
+
)),
|
|
104
|
+
!resizing && /* @__PURE__ */ jsxs(
|
|
105
|
+
"div",
|
|
106
|
+
{
|
|
107
|
+
className: "image-node__toolbar",
|
|
108
|
+
contentEditable: false,
|
|
109
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
110
|
+
children: [
|
|
111
|
+
/* @__PURE__ */ jsx(
|
|
112
|
+
"button",
|
|
113
|
+
{
|
|
114
|
+
type: "button",
|
|
115
|
+
onClick: () => setWrap("left"),
|
|
116
|
+
className: textWrap === "left" ? "active" : "",
|
|
117
|
+
title: "Text right",
|
|
118
|
+
children: /* @__PURE__ */ jsx(AlignLeft, { className: "w-4 h-4" })
|
|
119
|
+
}
|
|
120
|
+
),
|
|
121
|
+
/* @__PURE__ */ jsx(
|
|
122
|
+
"button",
|
|
123
|
+
{
|
|
124
|
+
type: "button",
|
|
125
|
+
onClick: () => setWrap("none"),
|
|
126
|
+
className: textWrap === "none" ? "active" : "",
|
|
127
|
+
title: "Full width",
|
|
128
|
+
children: /* @__PURE__ */ jsx(AlignCenter, { className: "w-4 h-4" })
|
|
129
|
+
}
|
|
130
|
+
),
|
|
131
|
+
/* @__PURE__ */ jsx(
|
|
132
|
+
"button",
|
|
133
|
+
{
|
|
134
|
+
type: "button",
|
|
135
|
+
onClick: () => setWrap("right"),
|
|
136
|
+
className: textWrap === "right" ? "active" : "",
|
|
137
|
+
title: "Text left",
|
|
138
|
+
children: /* @__PURE__ */ jsx(AlignRight, { className: "w-4 h-4" })
|
|
139
|
+
}
|
|
140
|
+
),
|
|
141
|
+
/* @__PURE__ */ jsx("span", { className: "image-node__toolbar-sep" }),
|
|
142
|
+
/* @__PURE__ */ jsx(
|
|
143
|
+
"button",
|
|
144
|
+
{
|
|
145
|
+
type: "button",
|
|
146
|
+
onClick: () => deleteNode(),
|
|
147
|
+
className: "delete",
|
|
148
|
+
title: "Remove",
|
|
149
|
+
children: /* @__PURE__ */ jsx(Trash2, { className: "w-4 h-4" })
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
] })
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/components/editor/extensions/ResizableImageExtension.ts
|
|
162
|
+
function figureAlignToWrap(figure) {
|
|
163
|
+
if (figure.classList.contains("align-left")) return "left";
|
|
164
|
+
if (figure.classList.contains("align-right")) return "right";
|
|
165
|
+
return "none";
|
|
166
|
+
}
|
|
167
|
+
function figureWidth(figure) {
|
|
168
|
+
return figure.style.maxWidth || figure.style.width || "100%";
|
|
169
|
+
}
|
|
170
|
+
var ResizableImage = Node.create({
|
|
171
|
+
name: "image",
|
|
172
|
+
group: "block",
|
|
173
|
+
draggable: true,
|
|
174
|
+
atom: true,
|
|
175
|
+
addOptions() {
|
|
176
|
+
return { HTMLAttributes: {} };
|
|
177
|
+
},
|
|
178
|
+
addAttributes() {
|
|
179
|
+
return {
|
|
180
|
+
src: { default: null },
|
|
181
|
+
alt: { default: null },
|
|
182
|
+
title: { default: null },
|
|
183
|
+
width: {
|
|
184
|
+
default: "100%",
|
|
185
|
+
parseHTML: (el) => {
|
|
186
|
+
const figure = el.closest("figure");
|
|
187
|
+
if (figure) return figureWidth(figure);
|
|
188
|
+
return el.style.width || "100%";
|
|
189
|
+
},
|
|
190
|
+
renderHTML: (attrs) => ({
|
|
191
|
+
style: `width: ${attrs.width}`
|
|
192
|
+
})
|
|
193
|
+
},
|
|
194
|
+
textWrap: {
|
|
195
|
+
default: "none",
|
|
196
|
+
parseHTML: (el) => {
|
|
197
|
+
const figure = el.closest("figure");
|
|
198
|
+
if (figure) return figureAlignToWrap(figure);
|
|
199
|
+
return el.getAttribute("data-text-wrap") || el.getAttribute("data-float") || "none";
|
|
200
|
+
},
|
|
201
|
+
renderHTML: (attrs) => ({
|
|
202
|
+
"data-text-wrap": attrs.textWrap
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
parseHTML() {
|
|
208
|
+
return [
|
|
209
|
+
{ tag: "figure img[src]" },
|
|
210
|
+
{ tag: "img[src]" }
|
|
211
|
+
];
|
|
212
|
+
},
|
|
213
|
+
renderHTML({ HTMLAttributes }) {
|
|
214
|
+
return [
|
|
215
|
+
"img",
|
|
216
|
+
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)
|
|
217
|
+
];
|
|
218
|
+
},
|
|
219
|
+
addCommands() {
|
|
220
|
+
return {
|
|
221
|
+
setImage: (options) => ({ commands }) => commands.insertContent({ type: this.name, attrs: options })
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
addNodeView() {
|
|
225
|
+
return ReactNodeViewRenderer(ResizableImageView);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// src/components/editor/ArticleEditor.tsx
|
|
230
|
+
import LinkExt from "@tiptap/extension-link";
|
|
231
|
+
|
|
232
|
+
// src/components/editor/extensions/DraggableYoutubeExtension.ts
|
|
233
|
+
import Youtube from "@tiptap/extension-youtube";
|
|
234
|
+
import { ReactNodeViewRenderer as ReactNodeViewRenderer2 } from "@tiptap/react";
|
|
235
|
+
|
|
236
|
+
// src/components/editor/extensions/DraggableYoutubeView.tsx
|
|
237
|
+
import { NodeViewWrapper as NodeViewWrapper2 } from "@tiptap/react";
|
|
238
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
239
|
+
function toEmbedUrl(url) {
|
|
240
|
+
if (!url) return "";
|
|
241
|
+
if (url.includes("/embed/")) return url;
|
|
242
|
+
const shortMatch = url.match(/youtu\.be\/([\w-]+)/);
|
|
243
|
+
if (shortMatch) return `https://www.youtube.com/embed/${shortMatch[1]}`;
|
|
244
|
+
const match = url.match(/(?:v=|shorts\/)([\w-]+)/);
|
|
245
|
+
if (match) return `https://www.youtube.com/embed/${match[1]}`;
|
|
246
|
+
return url;
|
|
247
|
+
}
|
|
248
|
+
function DraggableYoutubeView({ node, selected }) {
|
|
249
|
+
const { src, width, height } = node.attrs;
|
|
250
|
+
const embedUrl = toEmbedUrl(src ?? "");
|
|
251
|
+
if (!embedUrl) return /* @__PURE__ */ jsx2(NodeViewWrapper2, {});
|
|
252
|
+
return /* @__PURE__ */ jsx2(
|
|
253
|
+
NodeViewWrapper2,
|
|
254
|
+
{
|
|
255
|
+
className: `draggable-video-wrapper${selected ? " selected" : ""}`,
|
|
256
|
+
"data-drag-handle": true,
|
|
257
|
+
children: /* @__PURE__ */ jsx2(
|
|
258
|
+
"iframe",
|
|
259
|
+
{
|
|
260
|
+
src: embedUrl,
|
|
261
|
+
width: width || 640,
|
|
262
|
+
height: height || 360,
|
|
263
|
+
allowFullScreen: true,
|
|
264
|
+
allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
|
|
265
|
+
style: { pointerEvents: selected ? "none" : "auto" }
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/components/editor/extensions/DraggableYoutubeExtension.ts
|
|
273
|
+
var DraggableYoutube = Youtube.extend({
|
|
274
|
+
draggable: true,
|
|
275
|
+
addNodeView() {
|
|
276
|
+
return ReactNodeViewRenderer2(DraggableYoutubeView);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// src/components/editor/ArticleEditor.tsx
|
|
281
|
+
import Placeholder from "@tiptap/extension-placeholder";
|
|
282
|
+
|
|
283
|
+
// src/components/editor/extensions/VideoExtension.ts
|
|
284
|
+
import { Node as Node2, mergeAttributes as mergeAttributes2 } from "@tiptap/core";
|
|
285
|
+
import { ReactNodeViewRenderer as ReactNodeViewRenderer3 } from "@tiptap/react";
|
|
286
|
+
|
|
287
|
+
// src/components/editor/extensions/VideoView.tsx
|
|
288
|
+
import { NodeViewWrapper as NodeViewWrapper3 } from "@tiptap/react";
|
|
289
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
290
|
+
function VideoView({ node, selected }) {
|
|
291
|
+
const { src } = node.attrs;
|
|
292
|
+
if (!src) return /* @__PURE__ */ jsx3(NodeViewWrapper3, {});
|
|
293
|
+
return /* @__PURE__ */ jsx3(
|
|
294
|
+
NodeViewWrapper3,
|
|
295
|
+
{
|
|
296
|
+
className: `article-video draggable-video-wrapper${selected ? " selected" : ""}`,
|
|
297
|
+
"data-drag-handle": true,
|
|
298
|
+
children: /* @__PURE__ */ jsx3(
|
|
299
|
+
"video",
|
|
300
|
+
{
|
|
301
|
+
src,
|
|
302
|
+
controls: true,
|
|
303
|
+
style: { pointerEvents: selected ? "none" : "auto" }
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/components/editor/extensions/VideoExtension.ts
|
|
311
|
+
var VideoExtension = Node2.create({
|
|
312
|
+
name: "videoBlock",
|
|
313
|
+
group: "block",
|
|
314
|
+
atom: true,
|
|
315
|
+
draggable: true,
|
|
316
|
+
addAttributes() {
|
|
317
|
+
return {
|
|
318
|
+
src: { default: null }
|
|
319
|
+
};
|
|
320
|
+
},
|
|
321
|
+
parseHTML() {
|
|
322
|
+
return [{ tag: "video[src]" }];
|
|
323
|
+
},
|
|
324
|
+
renderHTML({ HTMLAttributes }) {
|
|
325
|
+
return [
|
|
326
|
+
"div",
|
|
327
|
+
{ class: "article-video" },
|
|
328
|
+
["video", mergeAttributes2(HTMLAttributes, { controls: "true" })]
|
|
329
|
+
];
|
|
330
|
+
},
|
|
331
|
+
addCommands() {
|
|
332
|
+
return {
|
|
333
|
+
setVideo: (options) => ({ commands }) => {
|
|
334
|
+
return commands.insertContent({
|
|
335
|
+
type: this.name,
|
|
336
|
+
attrs: options
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
addNodeView() {
|
|
342
|
+
return ReactNodeViewRenderer3(VideoView);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// src/components/editor/ArticleEditor.tsx
|
|
347
|
+
import UnderlineExt from "@tiptap/extension-underline";
|
|
348
|
+
import TextAlign from "@tiptap/extension-text-align";
|
|
349
|
+
import Highlight from "@tiptap/extension-highlight";
|
|
350
|
+
import { Color } from "@tiptap/extension-color";
|
|
351
|
+
import { TextStyle } from "@tiptap/extension-text-style";
|
|
352
|
+
import Superscript2 from "@tiptap/extension-superscript";
|
|
353
|
+
import Subscript2 from "@tiptap/extension-subscript";
|
|
354
|
+
import { Table as Table2 } from "@tiptap/extension-table";
|
|
355
|
+
import { TableRow } from "@tiptap/extension-table-row";
|
|
356
|
+
import { TableCell } from "@tiptap/extension-table-cell";
|
|
357
|
+
import { TableHeader } from "@tiptap/extension-table-header";
|
|
358
|
+
import {
|
|
359
|
+
ArrowLeft,
|
|
360
|
+
Check,
|
|
361
|
+
Loader2 as Loader22,
|
|
362
|
+
Settings,
|
|
363
|
+
ChevronRight,
|
|
364
|
+
Globe,
|
|
365
|
+
FileText,
|
|
366
|
+
History,
|
|
367
|
+
Eye
|
|
368
|
+
} from "lucide-react";
|
|
369
|
+
|
|
370
|
+
// src/components/editor/EditorToolbar.tsx
|
|
371
|
+
import {
|
|
372
|
+
Bold,
|
|
373
|
+
Italic,
|
|
374
|
+
Underline,
|
|
375
|
+
Strikethrough,
|
|
376
|
+
Superscript,
|
|
377
|
+
Subscript,
|
|
378
|
+
Heading2,
|
|
379
|
+
Heading3,
|
|
380
|
+
List,
|
|
381
|
+
ListOrdered,
|
|
382
|
+
Quote,
|
|
383
|
+
Minus,
|
|
384
|
+
Code,
|
|
385
|
+
AlignLeft as AlignLeft2,
|
|
386
|
+
AlignCenter as AlignCenter2,
|
|
387
|
+
AlignRight as AlignRight2,
|
|
388
|
+
Link,
|
|
389
|
+
Image as Image2,
|
|
390
|
+
Youtube as Youtube2,
|
|
391
|
+
Video,
|
|
392
|
+
Undo2,
|
|
393
|
+
Redo2,
|
|
394
|
+
Highlighter,
|
|
395
|
+
Palette,
|
|
396
|
+
Table,
|
|
397
|
+
Plus,
|
|
398
|
+
X as X2
|
|
399
|
+
} from "lucide-react";
|
|
400
|
+
import { useCallback as useCallback3, useState as useState3, useRef as useRef2, useEffect as useEffect2 } from "react";
|
|
401
|
+
|
|
402
|
+
// src/components/media/MediaPickerModal.tsx
|
|
403
|
+
import { useState as useState2, useEffect, useCallback as useCallback2 } from "react";
|
|
404
|
+
import { X, Search, Upload, Loader2, ImageIcon } from "lucide-react";
|
|
405
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
406
|
+
function formatSize(bytes) {
|
|
407
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
408
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
409
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
410
|
+
}
|
|
411
|
+
function MediaPickerModal({
|
|
412
|
+
open,
|
|
413
|
+
onClose,
|
|
414
|
+
onSelect
|
|
415
|
+
}) {
|
|
416
|
+
const actions = useGavaActions();
|
|
417
|
+
const { strings, upload: uploadConfig } = useGavaConfig();
|
|
418
|
+
const [tab, setTab] = useState2("library");
|
|
419
|
+
const [media, setMedia] = useState2([]);
|
|
420
|
+
const [loading, setLoading] = useState2(false);
|
|
421
|
+
const [search, setSearch] = useState2("");
|
|
422
|
+
const [uploading, setUploading] = useState2(false);
|
|
423
|
+
const [dragOver, setDragOver] = useState2(false);
|
|
424
|
+
const loadMedia = useCallback2(async () => {
|
|
425
|
+
setLoading(true);
|
|
426
|
+
try {
|
|
427
|
+
const data = await actions.getMedia();
|
|
428
|
+
setMedia(data);
|
|
429
|
+
} finally {
|
|
430
|
+
setLoading(false);
|
|
431
|
+
}
|
|
432
|
+
}, [actions]);
|
|
433
|
+
useEffect(() => {
|
|
434
|
+
if (open) {
|
|
435
|
+
loadMedia();
|
|
436
|
+
setSearch("");
|
|
437
|
+
setTab("library");
|
|
438
|
+
}
|
|
439
|
+
}, [open, loadMedia]);
|
|
440
|
+
useEffect(() => {
|
|
441
|
+
if (!open) return;
|
|
442
|
+
const handleKey = (e) => {
|
|
443
|
+
if (e.key === "Escape") onClose();
|
|
444
|
+
};
|
|
445
|
+
document.addEventListener("keydown", handleKey);
|
|
446
|
+
return () => document.removeEventListener("keydown", handleKey);
|
|
447
|
+
}, [open, onClose]);
|
|
448
|
+
const uploadFile = useCallback2(
|
|
449
|
+
async (file) => {
|
|
450
|
+
setUploading(true);
|
|
451
|
+
try {
|
|
452
|
+
const formData = new FormData();
|
|
453
|
+
formData.append("file", file);
|
|
454
|
+
const res = await fetch(actions.uploadUrl, {
|
|
455
|
+
method: "POST",
|
|
456
|
+
body: formData
|
|
457
|
+
});
|
|
458
|
+
const data = await res.json();
|
|
459
|
+
if (data.url) {
|
|
460
|
+
onSelect(data.url);
|
|
461
|
+
}
|
|
462
|
+
} finally {
|
|
463
|
+
setUploading(false);
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
[onSelect, actions.uploadUrl]
|
|
467
|
+
);
|
|
468
|
+
const handleFileSelect = useCallback2(() => {
|
|
469
|
+
const input = document.createElement("input");
|
|
470
|
+
input.type = "file";
|
|
471
|
+
input.accept = uploadConfig.imageTypes.join(",");
|
|
472
|
+
input.onchange = () => {
|
|
473
|
+
const file = input.files?.[0];
|
|
474
|
+
if (file) uploadFile(file);
|
|
475
|
+
};
|
|
476
|
+
input.click();
|
|
477
|
+
}, [uploadFile, uploadConfig.imageTypes]);
|
|
478
|
+
const handleDrop = useCallback2(
|
|
479
|
+
(e) => {
|
|
480
|
+
e.preventDefault();
|
|
481
|
+
setDragOver(false);
|
|
482
|
+
const file = e.dataTransfer.files[0];
|
|
483
|
+
if (file && file.type.startsWith("image/")) {
|
|
484
|
+
uploadFile(file);
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
[uploadFile]
|
|
488
|
+
);
|
|
489
|
+
if (!open) return null;
|
|
490
|
+
const images = media.filter((m) => m.mimeType.startsWith("image/"));
|
|
491
|
+
const filtered = images.filter(
|
|
492
|
+
(m) => search ? m.filename.toLowerCase().includes(search.toLowerCase()) : true
|
|
493
|
+
);
|
|
494
|
+
return /* @__PURE__ */ jsxs2("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
|
|
495
|
+
/* @__PURE__ */ jsx4("div", { className: "absolute inset-0 bg-black/50", onClick: onClose }),
|
|
496
|
+
/* @__PURE__ */ jsxs2("div", { className: "relative w-full max-w-3xl max-h-[85vh] mx-4 bg-card border border-card-border rounded-2xl shadow-xl flex flex-col dash-scale-in", children: [
|
|
497
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between px-5 py-4 border-b border-card-border", children: [
|
|
498
|
+
/* @__PURE__ */ jsx4("h2", { className: "text-lg font-semibold text-foreground", children: strings.selectImage }),
|
|
499
|
+
/* @__PURE__ */ jsx4(
|
|
500
|
+
"button",
|
|
501
|
+
{
|
|
502
|
+
onClick: onClose,
|
|
503
|
+
className: "p-1.5 rounded-full text-muted hover:bg-card-border/50 hover:text-foreground transition-colors",
|
|
504
|
+
children: /* @__PURE__ */ jsx4(X, { className: "w-5 h-5" })
|
|
505
|
+
}
|
|
506
|
+
)
|
|
507
|
+
] }),
|
|
508
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex border-b border-card-border px-5", children: [
|
|
509
|
+
/* @__PURE__ */ jsx4(
|
|
510
|
+
"button",
|
|
511
|
+
{
|
|
512
|
+
onClick: () => setTab("library"),
|
|
513
|
+
className: `px-4 py-2.5 text-sm font-medium border-b-2 transition-colors -mb-px ${tab === "library" ? "border-accent text-accent" : "border-transparent text-muted hover:text-foreground"}`,
|
|
514
|
+
children: strings.mediaLibrary
|
|
515
|
+
}
|
|
516
|
+
),
|
|
517
|
+
/* @__PURE__ */ jsx4(
|
|
518
|
+
"button",
|
|
519
|
+
{
|
|
520
|
+
onClick: () => setTab("upload"),
|
|
521
|
+
className: `px-4 py-2.5 text-sm font-medium border-b-2 transition-colors -mb-px ${tab === "upload" ? "border-accent text-accent" : "border-transparent text-muted hover:text-foreground"}`,
|
|
522
|
+
children: strings.uploadNew
|
|
523
|
+
}
|
|
524
|
+
)
|
|
525
|
+
] }),
|
|
526
|
+
/* @__PURE__ */ jsx4("div", { className: "flex-1 overflow-y-auto p-5", children: tab === "library" ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
527
|
+
/* @__PURE__ */ jsxs2("div", { className: "relative mb-4", children: [
|
|
528
|
+
/* @__PURE__ */ jsx4(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted" }),
|
|
529
|
+
/* @__PURE__ */ jsx4(
|
|
530
|
+
"input",
|
|
531
|
+
{
|
|
532
|
+
type: "text",
|
|
533
|
+
value: search,
|
|
534
|
+
onChange: (e) => setSearch(e.target.value),
|
|
535
|
+
placeholder: strings.searchFiles,
|
|
536
|
+
className: "w-full pl-9 pr-3 py-2 text-sm bg-background border border-card-border rounded-full outline-none focus:border-accent transition-colors"
|
|
537
|
+
}
|
|
538
|
+
)
|
|
539
|
+
] }),
|
|
540
|
+
loading ? /* @__PURE__ */ jsx4("div", { className: "flex items-center justify-center py-16", children: /* @__PURE__ */ jsx4(Loader2, { className: "w-6 h-6 text-muted animate-spin" }) }) : filtered.length === 0 ? /* @__PURE__ */ jsxs2("div", { className: "text-center py-16", children: [
|
|
541
|
+
/* @__PURE__ */ jsx4(ImageIcon, { className: "w-10 h-10 text-muted mx-auto mb-3" }),
|
|
542
|
+
/* @__PURE__ */ jsx4("p", { className: "text-sm text-muted", children: search ? strings.noImagesSearch : strings.noImages })
|
|
543
|
+
] }) : /* @__PURE__ */ jsx4("div", { className: "grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 gap-3", children: filtered.map((m) => /* @__PURE__ */ jsxs2(
|
|
544
|
+
"button",
|
|
545
|
+
{
|
|
546
|
+
type: "button",
|
|
547
|
+
onClick: () => onSelect(m.path),
|
|
548
|
+
className: "group relative aspect-square rounded-lg overflow-hidden border border-card-border hover:border-accent transition-colors focus:outline-none focus:ring-2 focus:ring-accent",
|
|
549
|
+
children: [
|
|
550
|
+
/* @__PURE__ */ jsx4(
|
|
551
|
+
"img",
|
|
552
|
+
{
|
|
553
|
+
src: m.path,
|
|
554
|
+
alt: m.filename,
|
|
555
|
+
className: "w-full h-full object-cover"
|
|
556
|
+
}
|
|
557
|
+
),
|
|
558
|
+
/* @__PURE__ */ jsx4("div", { className: "absolute inset-0 bg-black/0 group-hover:bg-black/30 transition-colors" }),
|
|
559
|
+
/* @__PURE__ */ jsxs2("div", { className: "absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-2 opacity-0 group-hover:opacity-100 transition-opacity", children: [
|
|
560
|
+
/* @__PURE__ */ jsx4("p", { className: "text-[11px] text-white truncate", children: m.filename }),
|
|
561
|
+
/* @__PURE__ */ jsx4("p", { className: "text-[10px] text-white/70", children: formatSize(m.size) })
|
|
562
|
+
] })
|
|
563
|
+
]
|
|
564
|
+
},
|
|
565
|
+
m.id
|
|
566
|
+
)) })
|
|
567
|
+
] }) : /* @__PURE__ */ jsx4(
|
|
568
|
+
"div",
|
|
569
|
+
{
|
|
570
|
+
onDragOver: (e) => {
|
|
571
|
+
e.preventDefault();
|
|
572
|
+
setDragOver(true);
|
|
573
|
+
},
|
|
574
|
+
onDragLeave: () => setDragOver(false),
|
|
575
|
+
onDrop: handleDrop,
|
|
576
|
+
className: `flex flex-col items-center justify-center py-20 rounded-xl border-2 border-dashed transition-colors ${dragOver ? "border-accent bg-accent/5" : "border-card-border"}`,
|
|
577
|
+
children: uploading ? /* @__PURE__ */ jsx4(Loader2, { className: "w-10 h-10 text-muted animate-spin" }) : /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
578
|
+
/* @__PURE__ */ jsx4(Upload, { className: "w-10 h-10 text-muted mb-4" }),
|
|
579
|
+
/* @__PURE__ */ jsx4("p", { className: "text-sm text-muted mb-4", children: strings.dragHint }),
|
|
580
|
+
/* @__PURE__ */ jsx4(
|
|
581
|
+
"button",
|
|
582
|
+
{
|
|
583
|
+
type: "button",
|
|
584
|
+
onClick: handleFileSelect,
|
|
585
|
+
className: "px-5 py-2 text-sm font-medium bg-accent text-white rounded-full hover:bg-accent/90 transition-colors",
|
|
586
|
+
children: strings.chooseFile
|
|
587
|
+
}
|
|
588
|
+
),
|
|
589
|
+
/* @__PURE__ */ jsx4("p", { className: "text-xs text-muted mt-3", children: strings.uploadHint })
|
|
590
|
+
] })
|
|
591
|
+
}
|
|
592
|
+
) })
|
|
593
|
+
] })
|
|
594
|
+
] });
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/components/editor/EditorToolbar.tsx
|
|
598
|
+
import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
599
|
+
var TEXT_COLORS = [
|
|
600
|
+
{ label: "Black", value: "#171717" },
|
|
601
|
+
{ label: "Red", value: "#dc2626" },
|
|
602
|
+
{ label: "Orange", value: "#ea580c" },
|
|
603
|
+
{ label: "Green", value: "#16a34a" },
|
|
604
|
+
{ label: "Blue", value: "#2563eb" },
|
|
605
|
+
{ label: "Purple", value: "#9333ea" },
|
|
606
|
+
{ label: "Gray", value: "#6b7280" }
|
|
607
|
+
];
|
|
608
|
+
var HIGHLIGHT_COLORS = [
|
|
609
|
+
{ label: "Yellow", value: "#fef08a" },
|
|
610
|
+
{ label: "Green", value: "#bbf7d0" },
|
|
611
|
+
{ label: "Blue", value: "#bfdbfe" },
|
|
612
|
+
{ label: "Pink", value: "#fbcfe8" },
|
|
613
|
+
{ label: "Orange", value: "#fed7aa" },
|
|
614
|
+
{ label: "Purple", value: "#e9d5ff" }
|
|
615
|
+
];
|
|
616
|
+
function EditorToolbar({ editor }) {
|
|
617
|
+
const actions = useGavaActions();
|
|
618
|
+
const [showColorPicker, setShowColorPicker] = useState3(false);
|
|
619
|
+
const [showHighlightPicker, setShowHighlightPicker] = useState3(false);
|
|
620
|
+
const [showTableMenu, setShowTableMenu] = useState3(false);
|
|
621
|
+
const [mediaPickerOpen, setMediaPickerOpen] = useState3(false);
|
|
622
|
+
const colorRef = useRef2(null);
|
|
623
|
+
const highlightRef = useRef2(null);
|
|
624
|
+
const tableRef = useRef2(null);
|
|
625
|
+
useEffect2(() => {
|
|
626
|
+
const handleClick = (e) => {
|
|
627
|
+
if (colorRef.current && !colorRef.current.contains(e.target))
|
|
628
|
+
setShowColorPicker(false);
|
|
629
|
+
if (highlightRef.current && !highlightRef.current.contains(e.target))
|
|
630
|
+
setShowHighlightPicker(false);
|
|
631
|
+
if (tableRef.current && !tableRef.current.contains(e.target))
|
|
632
|
+
setShowTableMenu(false);
|
|
633
|
+
};
|
|
634
|
+
document.addEventListener("mousedown", handleClick);
|
|
635
|
+
return () => document.removeEventListener("mousedown", handleClick);
|
|
636
|
+
}, []);
|
|
637
|
+
const handleImageSelect = useCallback3(
|
|
638
|
+
(url) => {
|
|
639
|
+
if (!editor) return;
|
|
640
|
+
editor.chain().focus().setImage({ src: url }).run();
|
|
641
|
+
setMediaPickerOpen(false);
|
|
642
|
+
},
|
|
643
|
+
[editor]
|
|
644
|
+
);
|
|
645
|
+
const addLink = useCallback3(() => {
|
|
646
|
+
if (!editor) return;
|
|
647
|
+
const previousUrl = editor.getAttributes("link").href;
|
|
648
|
+
const url = window.prompt("URL:", previousUrl);
|
|
649
|
+
if (url === null) return;
|
|
650
|
+
if (url === "") {
|
|
651
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
|
|
655
|
+
}, [editor]);
|
|
656
|
+
const addYoutube = useCallback3(() => {
|
|
657
|
+
if (!editor) return;
|
|
658
|
+
const url = window.prompt("YouTube URL:");
|
|
659
|
+
if (!url) return;
|
|
660
|
+
editor.commands.setYoutubeVideo({ src: url, width: 640, height: 360 });
|
|
661
|
+
}, [editor]);
|
|
662
|
+
const addVideo = useCallback3(() => {
|
|
663
|
+
if (!editor) return;
|
|
664
|
+
const input = document.createElement("input");
|
|
665
|
+
input.type = "file";
|
|
666
|
+
input.accept = "video/mp4,video/webm,video/quicktime";
|
|
667
|
+
input.onchange = async () => {
|
|
668
|
+
const file = input.files?.[0];
|
|
669
|
+
if (!file) return;
|
|
670
|
+
const formData = new FormData();
|
|
671
|
+
formData.append("file", file);
|
|
672
|
+
const res = await fetch(actions.uploadUrl, {
|
|
673
|
+
method: "POST",
|
|
674
|
+
body: formData
|
|
675
|
+
});
|
|
676
|
+
const data = await res.json();
|
|
677
|
+
if (data.url) {
|
|
678
|
+
editor.commands.setVideo({ src: data.url });
|
|
679
|
+
} else if (data.error) {
|
|
680
|
+
alert(data.error);
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
input.click();
|
|
684
|
+
}, [editor, actions.uploadUrl]);
|
|
685
|
+
if (!editor) return null;
|
|
686
|
+
const ToolButton = ({
|
|
687
|
+
onClick,
|
|
688
|
+
active,
|
|
689
|
+
children,
|
|
690
|
+
title
|
|
691
|
+
}) => /* @__PURE__ */ jsx5(
|
|
692
|
+
"button",
|
|
693
|
+
{
|
|
694
|
+
type: "button",
|
|
695
|
+
onClick,
|
|
696
|
+
title,
|
|
697
|
+
className: `p-1.5 rounded-full transition-colors ${active ? "bg-accent/15 text-accent" : "text-muted hover:bg-card-border/50 hover:text-foreground"}`,
|
|
698
|
+
children
|
|
699
|
+
}
|
|
700
|
+
);
|
|
701
|
+
const Divider = () => /* @__PURE__ */ jsx5("div", { className: "w-px h-6 bg-card-border mx-1" });
|
|
702
|
+
const DropdownItem = ({
|
|
703
|
+
onClick,
|
|
704
|
+
children,
|
|
705
|
+
danger
|
|
706
|
+
}) => /* @__PURE__ */ jsx5(
|
|
707
|
+
"button",
|
|
708
|
+
{
|
|
709
|
+
onClick,
|
|
710
|
+
className: `w-full text-left px-3 py-1.5 text-sm transition-colors ${danger ? "text-red-500 hover:bg-red-50" : "text-foreground hover:bg-card-border/50"}`,
|
|
711
|
+
children
|
|
712
|
+
}
|
|
713
|
+
);
|
|
714
|
+
return /* @__PURE__ */ jsxs3("div", { className: "inline-flex items-center flex-wrap gap-0.5 p-2 border border-card-border rounded-full bg-card", children: [
|
|
715
|
+
/* @__PURE__ */ jsx5(
|
|
716
|
+
ToolButton,
|
|
717
|
+
{
|
|
718
|
+
onClick: () => editor.chain().focus().toggleBold().run(),
|
|
719
|
+
active: editor.isActive("bold"),
|
|
720
|
+
title: "Bold",
|
|
721
|
+
children: /* @__PURE__ */ jsx5(Bold, { className: "w-4 h-4" })
|
|
722
|
+
}
|
|
723
|
+
),
|
|
724
|
+
/* @__PURE__ */ jsx5(
|
|
725
|
+
ToolButton,
|
|
726
|
+
{
|
|
727
|
+
onClick: () => editor.chain().focus().toggleItalic().run(),
|
|
728
|
+
active: editor.isActive("italic"),
|
|
729
|
+
title: "Italic",
|
|
730
|
+
children: /* @__PURE__ */ jsx5(Italic, { className: "w-4 h-4" })
|
|
731
|
+
}
|
|
732
|
+
),
|
|
733
|
+
/* @__PURE__ */ jsx5(
|
|
734
|
+
ToolButton,
|
|
735
|
+
{
|
|
736
|
+
onClick: () => editor.chain().focus().toggleUnderline().run(),
|
|
737
|
+
active: editor.isActive("underline"),
|
|
738
|
+
title: "Underline",
|
|
739
|
+
children: /* @__PURE__ */ jsx5(Underline, { className: "w-4 h-4" })
|
|
740
|
+
}
|
|
741
|
+
),
|
|
742
|
+
/* @__PURE__ */ jsx5(
|
|
743
|
+
ToolButton,
|
|
744
|
+
{
|
|
745
|
+
onClick: () => editor.chain().focus().toggleStrike().run(),
|
|
746
|
+
active: editor.isActive("strike"),
|
|
747
|
+
title: "Strikethrough",
|
|
748
|
+
children: /* @__PURE__ */ jsx5(Strikethrough, { className: "w-4 h-4" })
|
|
749
|
+
}
|
|
750
|
+
),
|
|
751
|
+
/* @__PURE__ */ jsx5(
|
|
752
|
+
ToolButton,
|
|
753
|
+
{
|
|
754
|
+
onClick: () => editor.chain().focus().toggleSuperscript().run(),
|
|
755
|
+
active: editor.isActive("superscript"),
|
|
756
|
+
title: "Superscript",
|
|
757
|
+
children: /* @__PURE__ */ jsx5(Superscript, { className: "w-4 h-4" })
|
|
758
|
+
}
|
|
759
|
+
),
|
|
760
|
+
/* @__PURE__ */ jsx5(
|
|
761
|
+
ToolButton,
|
|
762
|
+
{
|
|
763
|
+
onClick: () => editor.chain().focus().toggleSubscript().run(),
|
|
764
|
+
active: editor.isActive("subscript"),
|
|
765
|
+
title: "Subscript",
|
|
766
|
+
children: /* @__PURE__ */ jsx5(Subscript, { className: "w-4 h-4" })
|
|
767
|
+
}
|
|
768
|
+
),
|
|
769
|
+
/* @__PURE__ */ jsx5(Divider, {}),
|
|
770
|
+
/* @__PURE__ */ jsxs3("div", { className: "relative", ref: colorRef, children: [
|
|
771
|
+
/* @__PURE__ */ jsx5(
|
|
772
|
+
ToolButton,
|
|
773
|
+
{
|
|
774
|
+
onClick: () => {
|
|
775
|
+
setShowColorPicker(!showColorPicker);
|
|
776
|
+
setShowHighlightPicker(false);
|
|
777
|
+
setShowTableMenu(false);
|
|
778
|
+
},
|
|
779
|
+
active: showColorPicker,
|
|
780
|
+
title: "Text color",
|
|
781
|
+
children: /* @__PURE__ */ jsxs3("div", { className: "flex flex-col items-center gap-0.5", children: [
|
|
782
|
+
/* @__PURE__ */ jsx5(Palette, { className: "w-4 h-4" }),
|
|
783
|
+
/* @__PURE__ */ jsx5(
|
|
784
|
+
"div",
|
|
785
|
+
{
|
|
786
|
+
className: "w-3.5 h-0.5 rounded-full",
|
|
787
|
+
style: {
|
|
788
|
+
background: editor.getAttributes("textStyle").color || "currentColor"
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
)
|
|
792
|
+
] })
|
|
793
|
+
}
|
|
794
|
+
),
|
|
795
|
+
showColorPicker && /* @__PURE__ */ jsx5("div", { className: "absolute top-full left-0 mt-1 bg-card border border-card-border rounded-lg p-2 shadow-lg z-30", children: /* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-4 gap-1.5 w-[132px]", children: [
|
|
796
|
+
/* @__PURE__ */ jsx5(
|
|
797
|
+
"button",
|
|
798
|
+
{
|
|
799
|
+
onClick: () => {
|
|
800
|
+
editor.chain().focus().unsetColor().run();
|
|
801
|
+
setShowColorPicker(false);
|
|
802
|
+
},
|
|
803
|
+
className: "w-7 h-7 rounded-md border border-card-border hover:scale-110 transition-transform flex items-center justify-center bg-background",
|
|
804
|
+
title: "Default",
|
|
805
|
+
children: /* @__PURE__ */ jsx5(X2, { className: "w-3 h-3 text-muted" })
|
|
806
|
+
}
|
|
807
|
+
),
|
|
808
|
+
TEXT_COLORS.map((c) => /* @__PURE__ */ jsx5(
|
|
809
|
+
"button",
|
|
810
|
+
{
|
|
811
|
+
onClick: () => {
|
|
812
|
+
editor.chain().focus().setColor(c.value).run();
|
|
813
|
+
setShowColorPicker(false);
|
|
814
|
+
},
|
|
815
|
+
className: "w-7 h-7 rounded-md border border-card-border hover:scale-110 transition-transform",
|
|
816
|
+
style: { background: c.value },
|
|
817
|
+
title: c.label
|
|
818
|
+
},
|
|
819
|
+
c.value
|
|
820
|
+
))
|
|
821
|
+
] }) })
|
|
822
|
+
] }),
|
|
823
|
+
/* @__PURE__ */ jsxs3("div", { className: "relative", ref: highlightRef, children: [
|
|
824
|
+
/* @__PURE__ */ jsx5(
|
|
825
|
+
ToolButton,
|
|
826
|
+
{
|
|
827
|
+
onClick: () => {
|
|
828
|
+
setShowHighlightPicker(!showHighlightPicker);
|
|
829
|
+
setShowColorPicker(false);
|
|
830
|
+
setShowTableMenu(false);
|
|
831
|
+
},
|
|
832
|
+
active: editor.isActive("highlight"),
|
|
833
|
+
title: "Highlight",
|
|
834
|
+
children: /* @__PURE__ */ jsx5(Highlighter, { className: "w-4 h-4" })
|
|
835
|
+
}
|
|
836
|
+
),
|
|
837
|
+
showHighlightPicker && /* @__PURE__ */ jsx5("div", { className: "absolute top-full left-0 mt-1 bg-card border border-card-border rounded-lg p-2 shadow-lg z-30", children: /* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-4 gap-1.5 w-[132px]", children: [
|
|
838
|
+
/* @__PURE__ */ jsx5(
|
|
839
|
+
"button",
|
|
840
|
+
{
|
|
841
|
+
onClick: () => {
|
|
842
|
+
editor.chain().focus().unsetHighlight().run();
|
|
843
|
+
setShowHighlightPicker(false);
|
|
844
|
+
},
|
|
845
|
+
className: "w-7 h-7 rounded-md border border-card-border hover:scale-110 transition-transform flex items-center justify-center bg-background",
|
|
846
|
+
title: "Remove",
|
|
847
|
+
children: /* @__PURE__ */ jsx5(X2, { className: "w-3 h-3 text-muted" })
|
|
848
|
+
}
|
|
849
|
+
),
|
|
850
|
+
HIGHLIGHT_COLORS.map((c) => /* @__PURE__ */ jsx5(
|
|
851
|
+
"button",
|
|
852
|
+
{
|
|
853
|
+
onClick: () => {
|
|
854
|
+
editor.chain().focus().toggleHighlight({ color: c.value }).run();
|
|
855
|
+
setShowHighlightPicker(false);
|
|
856
|
+
},
|
|
857
|
+
className: "w-7 h-7 rounded-md border border-card-border hover:scale-110 transition-transform",
|
|
858
|
+
style: { background: c.value },
|
|
859
|
+
title: c.label
|
|
860
|
+
},
|
|
861
|
+
c.value
|
|
862
|
+
))
|
|
863
|
+
] }) })
|
|
864
|
+
] }),
|
|
865
|
+
/* @__PURE__ */ jsx5(Divider, {}),
|
|
866
|
+
/* @__PURE__ */ jsx5(
|
|
867
|
+
ToolButton,
|
|
868
|
+
{
|
|
869
|
+
onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
|
870
|
+
active: editor.isActive("heading", { level: 2 }),
|
|
871
|
+
title: "H2",
|
|
872
|
+
children: /* @__PURE__ */ jsx5(Heading2, { className: "w-4 h-4" })
|
|
873
|
+
}
|
|
874
|
+
),
|
|
875
|
+
/* @__PURE__ */ jsx5(
|
|
876
|
+
ToolButton,
|
|
877
|
+
{
|
|
878
|
+
onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
|
879
|
+
active: editor.isActive("heading", { level: 3 }),
|
|
880
|
+
title: "H3",
|
|
881
|
+
children: /* @__PURE__ */ jsx5(Heading3, { className: "w-4 h-4" })
|
|
882
|
+
}
|
|
883
|
+
),
|
|
884
|
+
/* @__PURE__ */ jsx5(Divider, {}),
|
|
885
|
+
/* @__PURE__ */ jsx5(
|
|
886
|
+
ToolButton,
|
|
887
|
+
{
|
|
888
|
+
onClick: () => editor.chain().focus().toggleBulletList().run(),
|
|
889
|
+
active: editor.isActive("bulletList"),
|
|
890
|
+
title: "Bullet list",
|
|
891
|
+
children: /* @__PURE__ */ jsx5(List, { className: "w-4 h-4" })
|
|
892
|
+
}
|
|
893
|
+
),
|
|
894
|
+
/* @__PURE__ */ jsx5(
|
|
895
|
+
ToolButton,
|
|
896
|
+
{
|
|
897
|
+
onClick: () => editor.chain().focus().toggleOrderedList().run(),
|
|
898
|
+
active: editor.isActive("orderedList"),
|
|
899
|
+
title: "Numbered list",
|
|
900
|
+
children: /* @__PURE__ */ jsx5(ListOrdered, { className: "w-4 h-4" })
|
|
901
|
+
}
|
|
902
|
+
),
|
|
903
|
+
/* @__PURE__ */ jsx5(
|
|
904
|
+
ToolButton,
|
|
905
|
+
{
|
|
906
|
+
onClick: () => editor.chain().focus().toggleBlockquote().run(),
|
|
907
|
+
active: editor.isActive("blockquote"),
|
|
908
|
+
title: "Quote",
|
|
909
|
+
children: /* @__PURE__ */ jsx5(Quote, { className: "w-4 h-4" })
|
|
910
|
+
}
|
|
911
|
+
),
|
|
912
|
+
/* @__PURE__ */ jsx5(
|
|
913
|
+
ToolButton,
|
|
914
|
+
{
|
|
915
|
+
onClick: () => editor.chain().focus().toggleCodeBlock().run(),
|
|
916
|
+
active: editor.isActive("codeBlock"),
|
|
917
|
+
title: "Code block",
|
|
918
|
+
children: /* @__PURE__ */ jsx5(Code, { className: "w-4 h-4" })
|
|
919
|
+
}
|
|
920
|
+
),
|
|
921
|
+
/* @__PURE__ */ jsx5(
|
|
922
|
+
ToolButton,
|
|
923
|
+
{
|
|
924
|
+
onClick: () => editor.chain().focus().setHorizontalRule().run(),
|
|
925
|
+
title: "Divider",
|
|
926
|
+
children: /* @__PURE__ */ jsx5(Minus, { className: "w-4 h-4" })
|
|
927
|
+
}
|
|
928
|
+
),
|
|
929
|
+
/* @__PURE__ */ jsx5(Divider, {}),
|
|
930
|
+
/* @__PURE__ */ jsx5(
|
|
931
|
+
ToolButton,
|
|
932
|
+
{
|
|
933
|
+
onClick: () => editor.chain().focus().setTextAlign("left").run(),
|
|
934
|
+
active: editor.isActive({ textAlign: "left" }),
|
|
935
|
+
title: "Align left",
|
|
936
|
+
children: /* @__PURE__ */ jsx5(AlignLeft2, { className: "w-4 h-4" })
|
|
937
|
+
}
|
|
938
|
+
),
|
|
939
|
+
/* @__PURE__ */ jsx5(
|
|
940
|
+
ToolButton,
|
|
941
|
+
{
|
|
942
|
+
onClick: () => editor.chain().focus().setTextAlign("center").run(),
|
|
943
|
+
active: editor.isActive({ textAlign: "center" }),
|
|
944
|
+
title: "Align center",
|
|
945
|
+
children: /* @__PURE__ */ jsx5(AlignCenter2, { className: "w-4 h-4" })
|
|
946
|
+
}
|
|
947
|
+
),
|
|
948
|
+
/* @__PURE__ */ jsx5(
|
|
949
|
+
ToolButton,
|
|
950
|
+
{
|
|
951
|
+
onClick: () => editor.chain().focus().setTextAlign("right").run(),
|
|
952
|
+
active: editor.isActive({ textAlign: "right" }),
|
|
953
|
+
title: "Align right",
|
|
954
|
+
children: /* @__PURE__ */ jsx5(AlignRight2, { className: "w-4 h-4" })
|
|
955
|
+
}
|
|
956
|
+
),
|
|
957
|
+
/* @__PURE__ */ jsx5(Divider, {}),
|
|
958
|
+
/* @__PURE__ */ jsxs3("div", { className: "relative", ref: tableRef, children: [
|
|
959
|
+
/* @__PURE__ */ jsx5(
|
|
960
|
+
ToolButton,
|
|
961
|
+
{
|
|
962
|
+
onClick: () => {
|
|
963
|
+
setShowTableMenu(!showTableMenu);
|
|
964
|
+
setShowColorPicker(false);
|
|
965
|
+
setShowHighlightPicker(false);
|
|
966
|
+
},
|
|
967
|
+
active: editor.isActive("table"),
|
|
968
|
+
title: "Table",
|
|
969
|
+
children: /* @__PURE__ */ jsx5(Table, { className: "w-4 h-4" })
|
|
970
|
+
}
|
|
971
|
+
),
|
|
972
|
+
showTableMenu && /* @__PURE__ */ jsx5("div", { className: "absolute top-full left-0 mt-1 bg-card border border-card-border rounded-lg py-1 shadow-lg z-30 w-52", children: !editor.isActive("table") ? /* @__PURE__ */ jsx5(
|
|
973
|
+
DropdownItem,
|
|
974
|
+
{
|
|
975
|
+
onClick: () => {
|
|
976
|
+
editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
|
|
977
|
+
setShowTableMenu(false);
|
|
978
|
+
},
|
|
979
|
+
children: /* @__PURE__ */ jsxs3("span", { className: "flex items-center gap-2", children: [
|
|
980
|
+
/* @__PURE__ */ jsx5(Plus, { className: "w-3.5 h-3.5" }),
|
|
981
|
+
"Insert 3x3 table"
|
|
982
|
+
] })
|
|
983
|
+
}
|
|
984
|
+
) : /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
985
|
+
/* @__PURE__ */ jsx5(
|
|
986
|
+
DropdownItem,
|
|
987
|
+
{
|
|
988
|
+
onClick: () => {
|
|
989
|
+
editor.chain().focus().addRowBefore().run();
|
|
990
|
+
setShowTableMenu(false);
|
|
991
|
+
},
|
|
992
|
+
children: "Add row above"
|
|
993
|
+
}
|
|
994
|
+
),
|
|
995
|
+
/* @__PURE__ */ jsx5(
|
|
996
|
+
DropdownItem,
|
|
997
|
+
{
|
|
998
|
+
onClick: () => {
|
|
999
|
+
editor.chain().focus().addRowAfter().run();
|
|
1000
|
+
setShowTableMenu(false);
|
|
1001
|
+
},
|
|
1002
|
+
children: "Add row below"
|
|
1003
|
+
}
|
|
1004
|
+
),
|
|
1005
|
+
/* @__PURE__ */ jsx5(
|
|
1006
|
+
DropdownItem,
|
|
1007
|
+
{
|
|
1008
|
+
onClick: () => {
|
|
1009
|
+
editor.chain().focus().addColumnBefore().run();
|
|
1010
|
+
setShowTableMenu(false);
|
|
1011
|
+
},
|
|
1012
|
+
children: "Add column left"
|
|
1013
|
+
}
|
|
1014
|
+
),
|
|
1015
|
+
/* @__PURE__ */ jsx5(
|
|
1016
|
+
DropdownItem,
|
|
1017
|
+
{
|
|
1018
|
+
onClick: () => {
|
|
1019
|
+
editor.chain().focus().addColumnAfter().run();
|
|
1020
|
+
setShowTableMenu(false);
|
|
1021
|
+
},
|
|
1022
|
+
children: "Add column right"
|
|
1023
|
+
}
|
|
1024
|
+
),
|
|
1025
|
+
/* @__PURE__ */ jsx5("div", { className: "border-t border-card-border my-1" }),
|
|
1026
|
+
/* @__PURE__ */ jsx5(
|
|
1027
|
+
DropdownItem,
|
|
1028
|
+
{
|
|
1029
|
+
danger: true,
|
|
1030
|
+
onClick: () => {
|
|
1031
|
+
editor.chain().focus().deleteRow().run();
|
|
1032
|
+
setShowTableMenu(false);
|
|
1033
|
+
},
|
|
1034
|
+
children: "Delete row"
|
|
1035
|
+
}
|
|
1036
|
+
),
|
|
1037
|
+
/* @__PURE__ */ jsx5(
|
|
1038
|
+
DropdownItem,
|
|
1039
|
+
{
|
|
1040
|
+
danger: true,
|
|
1041
|
+
onClick: () => {
|
|
1042
|
+
editor.chain().focus().deleteColumn().run();
|
|
1043
|
+
setShowTableMenu(false);
|
|
1044
|
+
},
|
|
1045
|
+
children: "Delete column"
|
|
1046
|
+
}
|
|
1047
|
+
),
|
|
1048
|
+
/* @__PURE__ */ jsx5(
|
|
1049
|
+
DropdownItem,
|
|
1050
|
+
{
|
|
1051
|
+
danger: true,
|
|
1052
|
+
onClick: () => {
|
|
1053
|
+
editor.chain().focus().deleteTable().run();
|
|
1054
|
+
setShowTableMenu(false);
|
|
1055
|
+
},
|
|
1056
|
+
children: "Delete table"
|
|
1057
|
+
}
|
|
1058
|
+
)
|
|
1059
|
+
] }) })
|
|
1060
|
+
] }),
|
|
1061
|
+
/* @__PURE__ */ jsx5(Divider, {}),
|
|
1062
|
+
/* @__PURE__ */ jsx5(ToolButton, { onClick: addLink, active: editor.isActive("link"), title: "Link", children: /* @__PURE__ */ jsx5(Link, { className: "w-4 h-4" }) }),
|
|
1063
|
+
/* @__PURE__ */ jsx5(ToolButton, { onClick: () => setMediaPickerOpen(true), title: "Image", children: /* @__PURE__ */ jsx5(Image2, { className: "w-4 h-4" }) }),
|
|
1064
|
+
/* @__PURE__ */ jsx5(ToolButton, { onClick: addYoutube, title: "YouTube", children: /* @__PURE__ */ jsx5(Youtube2, { className: "w-4 h-4" }) }),
|
|
1065
|
+
/* @__PURE__ */ jsx5(ToolButton, { onClick: addVideo, title: "Video", children: /* @__PURE__ */ jsx5(Video, { className: "w-4 h-4" }) }),
|
|
1066
|
+
/* @__PURE__ */ jsx5(Divider, {}),
|
|
1067
|
+
/* @__PURE__ */ jsx5(
|
|
1068
|
+
ToolButton,
|
|
1069
|
+
{
|
|
1070
|
+
onClick: () => editor.chain().focus().undo().run(),
|
|
1071
|
+
title: "Undo",
|
|
1072
|
+
children: /* @__PURE__ */ jsx5(Undo2, { className: "w-4 h-4" })
|
|
1073
|
+
}
|
|
1074
|
+
),
|
|
1075
|
+
/* @__PURE__ */ jsx5(
|
|
1076
|
+
ToolButton,
|
|
1077
|
+
{
|
|
1078
|
+
onClick: () => editor.chain().focus().redo().run(),
|
|
1079
|
+
title: "Redo",
|
|
1080
|
+
children: /* @__PURE__ */ jsx5(Redo2, { className: "w-4 h-4" })
|
|
1081
|
+
}
|
|
1082
|
+
),
|
|
1083
|
+
/* @__PURE__ */ jsx5(
|
|
1084
|
+
MediaPickerModal,
|
|
1085
|
+
{
|
|
1086
|
+
open: mediaPickerOpen,
|
|
1087
|
+
onClose: () => setMediaPickerOpen(false),
|
|
1088
|
+
onSelect: handleImageSelect
|
|
1089
|
+
}
|
|
1090
|
+
)
|
|
1091
|
+
] });
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// src/components/editor/CoverImageUpload.tsx
|
|
1095
|
+
import { useState as useState4 } from "react";
|
|
1096
|
+
import { ImagePlus, X as X3 } from "lucide-react";
|
|
1097
|
+
import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1098
|
+
function CoverImageUpload({ value, onChange }) {
|
|
1099
|
+
const [pickerOpen, setPickerOpen] = useState4(false);
|
|
1100
|
+
const { strings } = useGavaConfig();
|
|
1101
|
+
const handleSelect = (url) => {
|
|
1102
|
+
onChange(url);
|
|
1103
|
+
setPickerOpen(false);
|
|
1104
|
+
};
|
|
1105
|
+
if (value) {
|
|
1106
|
+
return /* @__PURE__ */ jsxs4(Fragment4, { children: [
|
|
1107
|
+
/* @__PURE__ */ jsxs4("div", { className: "relative aspect-[2/1] w-full rounded-xl overflow-hidden group cursor-pointer", children: [
|
|
1108
|
+
/* @__PURE__ */ jsx6(
|
|
1109
|
+
"img",
|
|
1110
|
+
{
|
|
1111
|
+
src: value,
|
|
1112
|
+
alt: strings.coverImageLabel,
|
|
1113
|
+
className: "w-full h-full object-cover"
|
|
1114
|
+
}
|
|
1115
|
+
),
|
|
1116
|
+
/* @__PURE__ */ jsx6("div", { className: "absolute inset-0 bg-black/0 group-hover:bg-black/30 transition-colors flex items-center justify-center", children: /* @__PURE__ */ jsxs4("div", { className: "opacity-0 group-hover:opacity-100 transition-opacity flex gap-2", children: [
|
|
1117
|
+
/* @__PURE__ */ jsx6(
|
|
1118
|
+
"button",
|
|
1119
|
+
{
|
|
1120
|
+
type: "button",
|
|
1121
|
+
onClick: (e) => {
|
|
1122
|
+
e.stopPropagation();
|
|
1123
|
+
setPickerOpen(true);
|
|
1124
|
+
},
|
|
1125
|
+
className: "bg-white text-gray-900 px-4 py-2 rounded-full text-sm font-medium hover:bg-gray-100 transition-colors",
|
|
1126
|
+
children: strings.change
|
|
1127
|
+
}
|
|
1128
|
+
),
|
|
1129
|
+
/* @__PURE__ */ jsx6(
|
|
1130
|
+
"button",
|
|
1131
|
+
{
|
|
1132
|
+
type: "button",
|
|
1133
|
+
onClick: (e) => {
|
|
1134
|
+
e.stopPropagation();
|
|
1135
|
+
onChange("");
|
|
1136
|
+
},
|
|
1137
|
+
className: "bg-red-500 text-white p-2 rounded-full hover:bg-red-600 transition-colors",
|
|
1138
|
+
children: /* @__PURE__ */ jsx6(X3, { className: "w-4 h-4" })
|
|
1139
|
+
}
|
|
1140
|
+
)
|
|
1141
|
+
] }) })
|
|
1142
|
+
] }),
|
|
1143
|
+
/* @__PURE__ */ jsx6(
|
|
1144
|
+
MediaPickerModal,
|
|
1145
|
+
{
|
|
1146
|
+
open: pickerOpen,
|
|
1147
|
+
onClose: () => setPickerOpen(false),
|
|
1148
|
+
onSelect: handleSelect
|
|
1149
|
+
}
|
|
1150
|
+
)
|
|
1151
|
+
] });
|
|
1152
|
+
}
|
|
1153
|
+
return /* @__PURE__ */ jsxs4(Fragment4, { children: [
|
|
1154
|
+
/* @__PURE__ */ jsxs4(
|
|
1155
|
+
"button",
|
|
1156
|
+
{
|
|
1157
|
+
type: "button",
|
|
1158
|
+
onClick: () => setPickerOpen(true),
|
|
1159
|
+
className: "w-full aspect-[2/1] rounded-xl border-2 border-dashed transition-colors flex flex-col items-center justify-center gap-3 border-card-border hover:border-muted",
|
|
1160
|
+
children: [
|
|
1161
|
+
/* @__PURE__ */ jsx6(ImagePlus, { className: "w-8 h-8 text-muted" }),
|
|
1162
|
+
/* @__PURE__ */ jsx6("div", { className: "text-sm text-muted", children: /* @__PURE__ */ jsx6("span", { className: "font-medium text-foreground", children: strings.coverImageLabel }) }),
|
|
1163
|
+
/* @__PURE__ */ jsx6("span", { className: "text-xs text-muted", children: strings.coverImageHint })
|
|
1164
|
+
]
|
|
1165
|
+
}
|
|
1166
|
+
),
|
|
1167
|
+
/* @__PURE__ */ jsx6(
|
|
1168
|
+
MediaPickerModal,
|
|
1169
|
+
{
|
|
1170
|
+
open: pickerOpen,
|
|
1171
|
+
onClose: () => setPickerOpen(false),
|
|
1172
|
+
onSelect: handleSelect
|
|
1173
|
+
}
|
|
1174
|
+
)
|
|
1175
|
+
] });
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// src/components/editor/RevisionPanel.tsx
|
|
1179
|
+
import { useState as useState5, useEffect as useEffect3 } from "react";
|
|
1180
|
+
import { X as X4, RotateCcw, Clock } from "lucide-react";
|
|
1181
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1182
|
+
function RevisionPanel({
|
|
1183
|
+
articleId,
|
|
1184
|
+
onClose,
|
|
1185
|
+
onRestore
|
|
1186
|
+
}) {
|
|
1187
|
+
const actions = useGavaActions();
|
|
1188
|
+
const { strings } = useGavaConfig();
|
|
1189
|
+
const [revisions, setRevisions] = useState5([]);
|
|
1190
|
+
const [loading, setLoading] = useState5(true);
|
|
1191
|
+
const [restoring, setRestoring] = useState5(null);
|
|
1192
|
+
useEffect3(() => {
|
|
1193
|
+
loadRevisions();
|
|
1194
|
+
}, [articleId]);
|
|
1195
|
+
async function loadRevisions() {
|
|
1196
|
+
setLoading(true);
|
|
1197
|
+
try {
|
|
1198
|
+
const data = await actions.getRevisions(articleId);
|
|
1199
|
+
setRevisions(data);
|
|
1200
|
+
} catch {
|
|
1201
|
+
} finally {
|
|
1202
|
+
setLoading(false);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
async function handleRestore(revisionId) {
|
|
1206
|
+
if (!confirm(strings.restoreConfirm)) return;
|
|
1207
|
+
setRestoring(revisionId);
|
|
1208
|
+
try {
|
|
1209
|
+
await actions.restoreRevision(articleId, revisionId);
|
|
1210
|
+
onRestore();
|
|
1211
|
+
} catch (err) {
|
|
1212
|
+
alert(err instanceof Error ? err.message : "Error restoring revision");
|
|
1213
|
+
} finally {
|
|
1214
|
+
setRestoring(null);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
function formatDate(date) {
|
|
1218
|
+
return new Date(date).toLocaleString("it-IT", {
|
|
1219
|
+
day: "numeric",
|
|
1220
|
+
month: "short",
|
|
1221
|
+
hour: "2-digit",
|
|
1222
|
+
minute: "2-digit"
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
return /* @__PURE__ */ jsxs5("div", { className: "w-80 border-l border-card-border bg-card overflow-auto p-5 hidden lg:block", children: [
|
|
1226
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between mb-4", children: [
|
|
1227
|
+
/* @__PURE__ */ jsxs5("h3", { className: "font-semibold text-foreground flex items-center gap-2", children: [
|
|
1228
|
+
/* @__PURE__ */ jsx7(Clock, { className: "w-4 h-4" }),
|
|
1229
|
+
strings.revisions
|
|
1230
|
+
] }),
|
|
1231
|
+
/* @__PURE__ */ jsx7(
|
|
1232
|
+
"button",
|
|
1233
|
+
{
|
|
1234
|
+
onClick: onClose,
|
|
1235
|
+
className: "p-1 rounded-full text-muted hover:text-foreground hover:bg-card-border/50 transition-colors",
|
|
1236
|
+
children: /* @__PURE__ */ jsx7(X4, { className: "w-4 h-4" })
|
|
1237
|
+
}
|
|
1238
|
+
)
|
|
1239
|
+
] }),
|
|
1240
|
+
loading ? /* @__PURE__ */ jsx7("p", { className: "text-sm text-muted", children: strings.loading }) : revisions.length === 0 ? /* @__PURE__ */ jsx7("p", { className: "text-sm text-muted", children: strings.noRevisions }) : /* @__PURE__ */ jsx7("div", { className: "space-y-3", children: revisions.map((rev) => /* @__PURE__ */ jsx7(
|
|
1241
|
+
"div",
|
|
1242
|
+
{
|
|
1243
|
+
className: "p-3 rounded-lg border border-card-border hover:border-accent/30 transition-colors",
|
|
1244
|
+
children: /* @__PURE__ */ jsxs5("div", { className: "flex items-start justify-between gap-2", children: [
|
|
1245
|
+
/* @__PURE__ */ jsxs5("div", { className: "min-w-0 flex-1", children: [
|
|
1246
|
+
/* @__PURE__ */ jsx7("p", { className: "text-sm font-medium text-foreground truncate", children: rev.title || strings.untitled }),
|
|
1247
|
+
/* @__PURE__ */ jsxs5("p", { className: "text-xs text-muted mt-0.5", children: [
|
|
1248
|
+
formatDate(rev.createdAt),
|
|
1249
|
+
" \u2014 ",
|
|
1250
|
+
rev.editor.name
|
|
1251
|
+
] }),
|
|
1252
|
+
rev.note && /* @__PURE__ */ jsx7("p", { className: "text-xs text-accent mt-1", children: rev.note })
|
|
1253
|
+
] }),
|
|
1254
|
+
/* @__PURE__ */ jsx7(
|
|
1255
|
+
"button",
|
|
1256
|
+
{
|
|
1257
|
+
onClick: () => handleRestore(rev.id),
|
|
1258
|
+
disabled: restoring === rev.id,
|
|
1259
|
+
className: "p-1.5 rounded-full text-muted hover:text-accent hover:bg-accent/10 transition-colors flex-shrink-0 disabled:opacity-50",
|
|
1260
|
+
title: strings.restore,
|
|
1261
|
+
children: /* @__PURE__ */ jsx7(
|
|
1262
|
+
RotateCcw,
|
|
1263
|
+
{
|
|
1264
|
+
className: `w-3.5 h-3.5 ${restoring === rev.id ? "animate-spin" : ""}`
|
|
1265
|
+
}
|
|
1266
|
+
)
|
|
1267
|
+
}
|
|
1268
|
+
)
|
|
1269
|
+
] })
|
|
1270
|
+
},
|
|
1271
|
+
rev.id
|
|
1272
|
+
)) })
|
|
1273
|
+
] });
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// src/components/editor/ArticleEditor.tsx
|
|
1277
|
+
import { Fragment as Fragment5, jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1278
|
+
function slugify(text) {
|
|
1279
|
+
return text.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1280
|
+
}
|
|
1281
|
+
function ArticleEditor({ article, canPublish: canPub }) {
|
|
1282
|
+
const router = useRouter();
|
|
1283
|
+
const actions = useGavaActions();
|
|
1284
|
+
const config = useGavaConfig();
|
|
1285
|
+
const { strings, routes, categories, editor: editorConfig } = config;
|
|
1286
|
+
const [title, setTitle] = useState6(article.title);
|
|
1287
|
+
const [slug, setSlug] = useState6(article.slug);
|
|
1288
|
+
const [excerpt, setExcerpt] = useState6(article.excerpt);
|
|
1289
|
+
const [coverImage, setCoverImage] = useState6(article.coverImage);
|
|
1290
|
+
const [category, setCategory] = useState6(article.category);
|
|
1291
|
+
const [status, setStatus] = useState6(article.status);
|
|
1292
|
+
const [authorName, setAuthorName] = useState6(article.authorName || "");
|
|
1293
|
+
const [saveStatus, setSaveStatus] = useState6("idle");
|
|
1294
|
+
const [showSettings, setShowSettings] = useState6(false);
|
|
1295
|
+
const [showHistory, setShowHistory] = useState6(false);
|
|
1296
|
+
const [autoSlug, setAutoSlug] = useState6(!article.slug);
|
|
1297
|
+
const saveTimeout = useRef3(null);
|
|
1298
|
+
const pendingUpdate = useRef3(false);
|
|
1299
|
+
const mountedRef = useRef3(false);
|
|
1300
|
+
const scrollRef = useRef3(null);
|
|
1301
|
+
const [scrolled, setScrolled] = useState6(false);
|
|
1302
|
+
const [hasSpace, setHasSpace] = useState6(false);
|
|
1303
|
+
const spread = scrolled && hasSpace;
|
|
1304
|
+
useEffect4(() => {
|
|
1305
|
+
document.documentElement.classList.remove("page-exit-to-editor");
|
|
1306
|
+
document.body.style.overflow = "hidden";
|
|
1307
|
+
return () => {
|
|
1308
|
+
document.body.style.overflow = "";
|
|
1309
|
+
};
|
|
1310
|
+
}, []);
|
|
1311
|
+
useEffect4(() => {
|
|
1312
|
+
const check = () => setHasSpace(window.innerWidth >= 1420);
|
|
1313
|
+
check();
|
|
1314
|
+
window.addEventListener("resize", check);
|
|
1315
|
+
return () => window.removeEventListener("resize", check);
|
|
1316
|
+
}, []);
|
|
1317
|
+
const editor = useEditor({
|
|
1318
|
+
immediatelyRender: false,
|
|
1319
|
+
extensions: [
|
|
1320
|
+
StarterKit.configure({
|
|
1321
|
+
heading: { levels: editorConfig.headingLevels }
|
|
1322
|
+
}),
|
|
1323
|
+
ResizableImage,
|
|
1324
|
+
LinkExt.configure({
|
|
1325
|
+
openOnClick: false,
|
|
1326
|
+
HTMLAttributes: { target: "_blank", rel: "noopener noreferrer" }
|
|
1327
|
+
}),
|
|
1328
|
+
DraggableYoutube.configure({
|
|
1329
|
+
HTMLAttributes: { class: "article-video-embed" },
|
|
1330
|
+
width: 640,
|
|
1331
|
+
height: 360
|
|
1332
|
+
}),
|
|
1333
|
+
Placeholder.configure({
|
|
1334
|
+
placeholder: editorConfig.placeholder
|
|
1335
|
+
}),
|
|
1336
|
+
UnderlineExt,
|
|
1337
|
+
TextAlign.configure({
|
|
1338
|
+
types: ["heading", "paragraph"]
|
|
1339
|
+
}),
|
|
1340
|
+
Highlight.configure({ multicolor: true }),
|
|
1341
|
+
Color,
|
|
1342
|
+
TextStyle,
|
|
1343
|
+
Superscript2,
|
|
1344
|
+
Subscript2,
|
|
1345
|
+
Table2.configure({ resizable: true }),
|
|
1346
|
+
TableRow,
|
|
1347
|
+
TableCell,
|
|
1348
|
+
TableHeader,
|
|
1349
|
+
VideoExtension
|
|
1350
|
+
],
|
|
1351
|
+
content: article.content || "",
|
|
1352
|
+
editorProps: {
|
|
1353
|
+
attributes: {
|
|
1354
|
+
class: "article-content outline-none min-h-[300px]"
|
|
1355
|
+
}
|
|
1356
|
+
},
|
|
1357
|
+
onUpdate: () => {
|
|
1358
|
+
scheduleSave();
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
const doSave = useCallback4(
|
|
1362
|
+
async (fields) => {
|
|
1363
|
+
if (!fields && !editor) return;
|
|
1364
|
+
if (pendingUpdate.current) return;
|
|
1365
|
+
pendingUpdate.current = true;
|
|
1366
|
+
setSaveStatus("saving");
|
|
1367
|
+
try {
|
|
1368
|
+
const data = fields ?? {
|
|
1369
|
+
title,
|
|
1370
|
+
slug,
|
|
1371
|
+
excerpt,
|
|
1372
|
+
coverImage,
|
|
1373
|
+
category,
|
|
1374
|
+
content: editor?.getHTML() ?? "",
|
|
1375
|
+
authorName
|
|
1376
|
+
};
|
|
1377
|
+
await actions.updateArticle(article.id, data);
|
|
1378
|
+
setSaveStatus("saved");
|
|
1379
|
+
setTimeout(() => setSaveStatus("idle"), 2e3);
|
|
1380
|
+
} catch {
|
|
1381
|
+
setSaveStatus("error");
|
|
1382
|
+
} finally {
|
|
1383
|
+
pendingUpdate.current = false;
|
|
1384
|
+
}
|
|
1385
|
+
},
|
|
1386
|
+
[article.id, title, slug, excerpt, coverImage, category, editor, authorName, actions]
|
|
1387
|
+
);
|
|
1388
|
+
const scheduleSave = useCallback4(() => {
|
|
1389
|
+
if (saveTimeout.current) clearTimeout(saveTimeout.current);
|
|
1390
|
+
saveTimeout.current = setTimeout(() => doSave(), editorConfig.autoSaveDelay);
|
|
1391
|
+
}, [doSave, editorConfig.autoSaveDelay]);
|
|
1392
|
+
useEffect4(() => {
|
|
1393
|
+
const handleKeyDown = (e) => {
|
|
1394
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
|
|
1395
|
+
e.preventDefault();
|
|
1396
|
+
if (saveTimeout.current) clearTimeout(saveTimeout.current);
|
|
1397
|
+
doSave();
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
1401
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
1402
|
+
}, [doSave]);
|
|
1403
|
+
useEffect4(() => {
|
|
1404
|
+
if (!mountedRef.current) {
|
|
1405
|
+
mountedRef.current = true;
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
scheduleSave();
|
|
1409
|
+
}, [title, slug, excerpt, coverImage, category, authorName]);
|
|
1410
|
+
useEffect4(() => {
|
|
1411
|
+
const el = scrollRef.current;
|
|
1412
|
+
if (!el) return;
|
|
1413
|
+
const onScroll = () => setScrolled(el.scrollTop > 50);
|
|
1414
|
+
onScroll();
|
|
1415
|
+
el.addEventListener("scroll", onScroll, { passive: true });
|
|
1416
|
+
return () => el.removeEventListener("scroll", onScroll);
|
|
1417
|
+
}, []);
|
|
1418
|
+
useEffect4(() => {
|
|
1419
|
+
if (autoSlug && title) {
|
|
1420
|
+
setSlug(slugify(title));
|
|
1421
|
+
}
|
|
1422
|
+
}, [title, autoSlug]);
|
|
1423
|
+
const handlePublish = async () => {
|
|
1424
|
+
if (saveTimeout.current) clearTimeout(saveTimeout.current);
|
|
1425
|
+
await doSave();
|
|
1426
|
+
try {
|
|
1427
|
+
await actions.publishArticle(article.id);
|
|
1428
|
+
setStatus("pubblicato");
|
|
1429
|
+
router.refresh();
|
|
1430
|
+
} catch (err) {
|
|
1431
|
+
alert(err instanceof Error ? err.message : "Error publishing");
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
const handleUnpublish = async () => {
|
|
1435
|
+
try {
|
|
1436
|
+
await actions.unpublishArticle(article.id);
|
|
1437
|
+
setStatus("bozza");
|
|
1438
|
+
router.refresh();
|
|
1439
|
+
} catch (err) {
|
|
1440
|
+
alert(err instanceof Error ? err.message : "Error");
|
|
1441
|
+
}
|
|
1442
|
+
};
|
|
1443
|
+
const handleSaveDraft = async () => {
|
|
1444
|
+
if (saveTimeout.current) clearTimeout(saveTimeout.current);
|
|
1445
|
+
await doSave();
|
|
1446
|
+
};
|
|
1447
|
+
return /* @__PURE__ */ jsxs6("div", { className: "-mx-4 sm:-mx-6 -my-6 lg:-my-8 h-[calc(100dvh-4.5rem)] flex flex-col bg-background overflow-hidden", children: [
|
|
1448
|
+
/* @__PURE__ */ jsx8(
|
|
1449
|
+
"div",
|
|
1450
|
+
{
|
|
1451
|
+
className: `z-20 px-4 py-3 pointer-events-none transition-all duration-500 ease-out ${spread ? "absolute top-[4.5rem] left-0 right-0" : "flex-shrink-0 relative"}`,
|
|
1452
|
+
children: /* @__PURE__ */ jsxs6(
|
|
1453
|
+
"div",
|
|
1454
|
+
{
|
|
1455
|
+
className: "flex items-center justify-between mx-auto transition-all duration-500 ease-out",
|
|
1456
|
+
style: { maxWidth: spread ? "100%" : "64rem" },
|
|
1457
|
+
children: [
|
|
1458
|
+
/* @__PURE__ */ jsx8(
|
|
1459
|
+
"div",
|
|
1460
|
+
{
|
|
1461
|
+
className: `flex items-center pointer-events-auto transition-all duration-500 ease-out ${spread ? "bg-background/90 backdrop-blur-md shadow-lg border border-card-border/50 rounded-full px-3 py-1.5" : ""}`,
|
|
1462
|
+
children: /* @__PURE__ */ jsxs6(
|
|
1463
|
+
"button",
|
|
1464
|
+
{
|
|
1465
|
+
onClick: () => router.push(routes.articles),
|
|
1466
|
+
className: "flex items-center gap-2 text-sm text-muted hover:text-foreground transition-colors",
|
|
1467
|
+
children: [
|
|
1468
|
+
/* @__PURE__ */ jsx8(ArrowLeft, { className: "w-4 h-4" }),
|
|
1469
|
+
/* @__PURE__ */ jsx8("span", { className: "hidden sm:inline", children: strings.articles })
|
|
1470
|
+
]
|
|
1471
|
+
}
|
|
1472
|
+
)
|
|
1473
|
+
}
|
|
1474
|
+
),
|
|
1475
|
+
/* @__PURE__ */ jsxs6(
|
|
1476
|
+
"div",
|
|
1477
|
+
{
|
|
1478
|
+
className: `flex items-center gap-2 text-sm text-muted transition-all duration-500 ${spread ? "opacity-0 scale-90 pointer-events-none" : "opacity-100 scale-100"}`,
|
|
1479
|
+
children: [
|
|
1480
|
+
saveStatus === "saving" && /* @__PURE__ */ jsxs6(Fragment5, { children: [
|
|
1481
|
+
/* @__PURE__ */ jsx8(Loader22, { className: "w-3.5 h-3.5 animate-spin" }),
|
|
1482
|
+
/* @__PURE__ */ jsx8("span", { children: strings.saving })
|
|
1483
|
+
] }),
|
|
1484
|
+
saveStatus === "saved" && /* @__PURE__ */ jsxs6(Fragment5, { children: [
|
|
1485
|
+
/* @__PURE__ */ jsx8(Check, { className: "w-3.5 h-3.5 text-green-500" }),
|
|
1486
|
+
/* @__PURE__ */ jsx8("span", { className: "text-green-500", children: strings.saved })
|
|
1487
|
+
] }),
|
|
1488
|
+
saveStatus === "error" && /* @__PURE__ */ jsx8("span", { className: "text-red-500", children: strings.saveError })
|
|
1489
|
+
]
|
|
1490
|
+
}
|
|
1491
|
+
),
|
|
1492
|
+
/* @__PURE__ */ jsxs6(
|
|
1493
|
+
"div",
|
|
1494
|
+
{
|
|
1495
|
+
className: `flex items-center gap-2 pointer-events-auto transition-all duration-500 ease-out ${spread ? "bg-background/90 backdrop-blur-md shadow-lg border border-card-border/50 rounded-full px-2 py-1" : ""}`,
|
|
1496
|
+
children: [
|
|
1497
|
+
/* @__PURE__ */ jsxs6(
|
|
1498
|
+
"div",
|
|
1499
|
+
{
|
|
1500
|
+
className: `flex items-center gap-1.5 text-sm text-muted overflow-hidden transition-all duration-300 ${spread && saveStatus !== "idle" ? "max-w-[30px] opacity-100 pl-1" : "max-w-0 opacity-0"}`,
|
|
1501
|
+
children: [
|
|
1502
|
+
saveStatus === "saving" && /* @__PURE__ */ jsx8(Loader22, { className: "w-3.5 h-3.5 animate-spin flex-shrink-0" }),
|
|
1503
|
+
saveStatus === "saved" && /* @__PURE__ */ jsx8(Check, { className: "w-3.5 h-3.5 text-green-500 flex-shrink-0" }),
|
|
1504
|
+
saveStatus === "error" && /* @__PURE__ */ jsx8("span", { className: "w-2 h-2 rounded-full bg-red-500 flex-shrink-0" })
|
|
1505
|
+
]
|
|
1506
|
+
}
|
|
1507
|
+
),
|
|
1508
|
+
/* @__PURE__ */ jsx8(
|
|
1509
|
+
"button",
|
|
1510
|
+
{
|
|
1511
|
+
onClick: () => window.open(
|
|
1512
|
+
status === "pubblicato" && slug ? routes.articleView(slug) : routes.articlePreview(article.id),
|
|
1513
|
+
"_blank"
|
|
1514
|
+
),
|
|
1515
|
+
className: "p-2 rounded-full text-muted hover:text-foreground hover:bg-card-border/50 transition-colors",
|
|
1516
|
+
title: status === "pubblicato" ? strings.viewArticle : strings.preview,
|
|
1517
|
+
children: /* @__PURE__ */ jsx8(Eye, { className: "w-4 h-4" })
|
|
1518
|
+
}
|
|
1519
|
+
),
|
|
1520
|
+
/* @__PURE__ */ jsx8(
|
|
1521
|
+
"button",
|
|
1522
|
+
{
|
|
1523
|
+
onClick: () => {
|
|
1524
|
+
setShowHistory(!showHistory);
|
|
1525
|
+
if (!showHistory) setShowSettings(false);
|
|
1526
|
+
},
|
|
1527
|
+
className: `p-2 rounded-full transition-colors ${showHistory ? "bg-accent/10 text-accent" : "text-muted hover:text-foreground hover:bg-card-border/50"}`,
|
|
1528
|
+
title: strings.revisions,
|
|
1529
|
+
children: /* @__PURE__ */ jsx8(History, { className: "w-4 h-4" })
|
|
1530
|
+
}
|
|
1531
|
+
),
|
|
1532
|
+
/* @__PURE__ */ jsx8(
|
|
1533
|
+
"button",
|
|
1534
|
+
{
|
|
1535
|
+
onClick: () => {
|
|
1536
|
+
setShowSettings(!showSettings);
|
|
1537
|
+
if (!showSettings) setShowHistory(false);
|
|
1538
|
+
},
|
|
1539
|
+
className: `p-2 rounded-full transition-colors ${showSettings ? "bg-accent/10 text-accent" : "text-muted hover:text-foreground hover:bg-card-border/50"}`,
|
|
1540
|
+
title: strings.settings,
|
|
1541
|
+
children: /* @__PURE__ */ jsx8(Settings, { className: "w-4 h-4" })
|
|
1542
|
+
}
|
|
1543
|
+
),
|
|
1544
|
+
/* @__PURE__ */ jsx8(
|
|
1545
|
+
"button",
|
|
1546
|
+
{
|
|
1547
|
+
onClick: handleSaveDraft,
|
|
1548
|
+
className: "px-3 py-1.5 text-sm border border-card-border/50 rounded-full hover:bg-card-border/50 transition-colors",
|
|
1549
|
+
children: strings.saveDraft
|
|
1550
|
+
}
|
|
1551
|
+
),
|
|
1552
|
+
canPub && status === "bozza" && /* @__PURE__ */ jsxs6(
|
|
1553
|
+
"button",
|
|
1554
|
+
{
|
|
1555
|
+
onClick: handlePublish,
|
|
1556
|
+
className: "px-4 py-1.5 text-sm bg-accent text-white rounded-full hover:bg-accent-hover transition-colors font-medium flex items-center gap-1.5",
|
|
1557
|
+
children: [
|
|
1558
|
+
/* @__PURE__ */ jsx8(Globe, { className: "w-3.5 h-3.5" }),
|
|
1559
|
+
strings.publish
|
|
1560
|
+
]
|
|
1561
|
+
}
|
|
1562
|
+
),
|
|
1563
|
+
canPub && status === "pubblicato" && /* @__PURE__ */ jsx8(
|
|
1564
|
+
"button",
|
|
1565
|
+
{
|
|
1566
|
+
onClick: handleUnpublish,
|
|
1567
|
+
className: "px-3 py-1.5 text-sm border border-amber-500 text-amber-600 rounded-full hover:bg-amber-50 transition-colors",
|
|
1568
|
+
children: strings.unpublish
|
|
1569
|
+
}
|
|
1570
|
+
)
|
|
1571
|
+
]
|
|
1572
|
+
}
|
|
1573
|
+
)
|
|
1574
|
+
]
|
|
1575
|
+
}
|
|
1576
|
+
)
|
|
1577
|
+
}
|
|
1578
|
+
),
|
|
1579
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex-1 flex min-h-0", children: [
|
|
1580
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex-1 overflow-y-auto", ref: scrollRef, children: [
|
|
1581
|
+
/* @__PURE__ */ jsxs6("div", { className: "article-content-wrapper py-8", children: [
|
|
1582
|
+
/* @__PURE__ */ jsx8("div", { className: "flex items-center gap-2 mb-6", children: status === "pubblicato" ? /* @__PURE__ */ jsxs6("span", { className: "inline-flex items-center gap-1.5 text-xs font-medium text-green-600 bg-green-50 px-2.5 py-1 rounded-full", children: [
|
|
1583
|
+
/* @__PURE__ */ jsx8(Globe, { className: "w-3 h-3" }),
|
|
1584
|
+
strings.published
|
|
1585
|
+
] }) : /* @__PURE__ */ jsxs6("span", { className: "inline-flex items-center gap-1.5 text-xs font-medium text-amber-600 bg-amber-50 px-2.5 py-1 rounded-full", children: [
|
|
1586
|
+
/* @__PURE__ */ jsx8(FileText, { className: "w-3 h-3" }),
|
|
1587
|
+
strings.draft
|
|
1588
|
+
] }) }),
|
|
1589
|
+
/* @__PURE__ */ jsx8(CoverImageUpload, { value: coverImage, onChange: setCoverImage }),
|
|
1590
|
+
/* @__PURE__ */ jsx8(
|
|
1591
|
+
"textarea",
|
|
1592
|
+
{
|
|
1593
|
+
ref: (el) => {
|
|
1594
|
+
if (el) {
|
|
1595
|
+
el.style.height = "auto";
|
|
1596
|
+
el.style.height = el.scrollHeight + "px";
|
|
1597
|
+
}
|
|
1598
|
+
},
|
|
1599
|
+
value: title,
|
|
1600
|
+
onChange: (e) => {
|
|
1601
|
+
setTitle(e.target.value);
|
|
1602
|
+
const el = e.target;
|
|
1603
|
+
el.style.height = "auto";
|
|
1604
|
+
el.style.height = el.scrollHeight + "px";
|
|
1605
|
+
},
|
|
1606
|
+
rows: 1,
|
|
1607
|
+
placeholder: strings.titlePlaceholder,
|
|
1608
|
+
className: "w-full mt-8 mb-6 text-4xl md:text-[2.75rem] lg:text-5xl font-heading font-bold text-foreground leading-[1.08] tracking-[-0.02em] bg-transparent border-none outline-none placeholder:text-muted-foreground/40 resize-none overflow-hidden"
|
|
1609
|
+
}
|
|
1610
|
+
)
|
|
1611
|
+
] }),
|
|
1612
|
+
/* @__PURE__ */ jsx8("div", { className: "sticky top-0 z-10 py-2 flex justify-center px-4", children: /* @__PURE__ */ jsx8(EditorToolbar, { editor }) }),
|
|
1613
|
+
/* @__PURE__ */ jsx8("div", { className: "article-content-wrapper pb-8", children: /* @__PURE__ */ jsx8(EditorContent, { editor }) })
|
|
1614
|
+
] }),
|
|
1615
|
+
showHistory && /* @__PURE__ */ jsx8(
|
|
1616
|
+
RevisionPanel,
|
|
1617
|
+
{
|
|
1618
|
+
articleId: article.id,
|
|
1619
|
+
onClose: () => setShowHistory(false),
|
|
1620
|
+
onRestore: () => router.refresh()
|
|
1621
|
+
}
|
|
1622
|
+
),
|
|
1623
|
+
showSettings && /* @__PURE__ */ jsxs6("div", { className: "w-80 border-l border-card-border bg-card overflow-auto p-5 hidden lg:block", children: [
|
|
1624
|
+
/* @__PURE__ */ jsxs6("h3", { className: "font-semibold text-foreground mb-4 flex items-center gap-2", children: [
|
|
1625
|
+
/* @__PURE__ */ jsx8(Settings, { className: "w-4 h-4" }),
|
|
1626
|
+
strings.settings
|
|
1627
|
+
] }),
|
|
1628
|
+
/* @__PURE__ */ jsxs6("div", { className: "space-y-5", children: [
|
|
1629
|
+
/* @__PURE__ */ jsxs6("div", { children: [
|
|
1630
|
+
/* @__PURE__ */ jsx8("label", { className: "block text-sm font-medium text-foreground mb-1.5", children: strings.slugUrl }),
|
|
1631
|
+
/* @__PURE__ */ jsx8("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsx8(
|
|
1632
|
+
"input",
|
|
1633
|
+
{
|
|
1634
|
+
type: "text",
|
|
1635
|
+
value: slug,
|
|
1636
|
+
onChange: (e) => {
|
|
1637
|
+
setAutoSlug(false);
|
|
1638
|
+
setSlug(e.target.value);
|
|
1639
|
+
},
|
|
1640
|
+
placeholder: strings.slugPlaceholder,
|
|
1641
|
+
className: "flex-1 px-3 py-2 text-sm bg-background border border-card-border rounded-lg outline-none focus:border-accent"
|
|
1642
|
+
}
|
|
1643
|
+
) }),
|
|
1644
|
+
/* @__PURE__ */ jsx8("p", { className: "text-xs text-muted mt-1", children: routes.articleView(slug || "...") })
|
|
1645
|
+
] }),
|
|
1646
|
+
/* @__PURE__ */ jsxs6("div", { children: [
|
|
1647
|
+
/* @__PURE__ */ jsx8("label", { className: "block text-sm font-medium text-foreground mb-1.5", children: strings.category }),
|
|
1648
|
+
/* @__PURE__ */ jsxs6(
|
|
1649
|
+
"select",
|
|
1650
|
+
{
|
|
1651
|
+
value: category,
|
|
1652
|
+
onChange: (e) => setCategory(e.target.value),
|
|
1653
|
+
className: "w-full px-3 py-2 text-sm bg-background border border-card-border rounded-lg outline-none focus:border-accent",
|
|
1654
|
+
children: [
|
|
1655
|
+
/* @__PURE__ */ jsx8("option", { value: "", children: strings.selectCategory }),
|
|
1656
|
+
categories.map((cat) => /* @__PURE__ */ jsx8("option", { value: cat, children: cat }, cat))
|
|
1657
|
+
]
|
|
1658
|
+
}
|
|
1659
|
+
)
|
|
1660
|
+
] }),
|
|
1661
|
+
/* @__PURE__ */ jsxs6("div", { children: [
|
|
1662
|
+
/* @__PURE__ */ jsx8("label", { className: "block text-sm font-medium text-foreground mb-1.5", children: strings.excerpt }),
|
|
1663
|
+
/* @__PURE__ */ jsx8(
|
|
1664
|
+
"textarea",
|
|
1665
|
+
{
|
|
1666
|
+
value: excerpt,
|
|
1667
|
+
onChange: (e) => setExcerpt(e.target.value),
|
|
1668
|
+
placeholder: strings.excerptPlaceholder,
|
|
1669
|
+
rows: 3,
|
|
1670
|
+
className: "w-full px-3 py-2 text-sm bg-background border border-card-border rounded-lg outline-none focus:border-accent resize-none"
|
|
1671
|
+
}
|
|
1672
|
+
)
|
|
1673
|
+
] }),
|
|
1674
|
+
/* @__PURE__ */ jsx8("div", { className: "pt-4 border-t border-card-border", children: /* @__PURE__ */ jsxs6("div", { children: [
|
|
1675
|
+
/* @__PURE__ */ jsx8("label", { className: "block text-sm font-medium text-foreground mb-1.5", children: strings.author }),
|
|
1676
|
+
/* @__PURE__ */ jsx8(
|
|
1677
|
+
"input",
|
|
1678
|
+
{
|
|
1679
|
+
type: "text",
|
|
1680
|
+
value: authorName,
|
|
1681
|
+
onChange: (e) => setAuthorName(e.target.value),
|
|
1682
|
+
placeholder: strings.authorPlaceholder,
|
|
1683
|
+
className: "w-full px-3 py-2 text-sm bg-background border border-card-border rounded-lg outline-none focus:border-accent"
|
|
1684
|
+
}
|
|
1685
|
+
)
|
|
1686
|
+
] }) }),
|
|
1687
|
+
status === "pubblicato" && slug && /* @__PURE__ */ jsx8("div", { children: /* @__PURE__ */ jsxs6(
|
|
1688
|
+
"a",
|
|
1689
|
+
{
|
|
1690
|
+
href: routes.articleView(slug),
|
|
1691
|
+
target: "_blank",
|
|
1692
|
+
rel: "noopener noreferrer",
|
|
1693
|
+
className: "flex items-center gap-2 text-sm text-accent hover:underline",
|
|
1694
|
+
children: [
|
|
1695
|
+
strings.viewArticle,
|
|
1696
|
+
/* @__PURE__ */ jsx8(ChevronRight, { className: "w-3.5 h-3.5" })
|
|
1697
|
+
]
|
|
1698
|
+
}
|
|
1699
|
+
) })
|
|
1700
|
+
] })
|
|
1701
|
+
] })
|
|
1702
|
+
] })
|
|
1703
|
+
] });
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// src/components/editor/ImageEditModal.tsx
|
|
1707
|
+
import { useState as useState7, useCallback as useCallback5 } from "react";
|
|
1708
|
+
import Cropper from "react-easy-crop";
|
|
1709
|
+
import {
|
|
1710
|
+
X as X5,
|
|
1711
|
+
ZoomIn,
|
|
1712
|
+
ZoomOut,
|
|
1713
|
+
Check as Check2,
|
|
1714
|
+
RectangleHorizontal,
|
|
1715
|
+
Square,
|
|
1716
|
+
Smartphone,
|
|
1717
|
+
Maximize,
|
|
1718
|
+
Loader2 as Loader23,
|
|
1719
|
+
RotateCcw as RotateCcw2
|
|
1720
|
+
} from "lucide-react";
|
|
1721
|
+
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1722
|
+
var ASPECT_PRESETS = [
|
|
1723
|
+
{ label: "Free", value: void 0, icon: Maximize },
|
|
1724
|
+
{ label: "16:9", value: 16 / 9, icon: RectangleHorizontal },
|
|
1725
|
+
{ label: "4:3", value: 4 / 3, icon: Smartphone },
|
|
1726
|
+
{ label: "1:1", value: 1, icon: Square }
|
|
1727
|
+
];
|
|
1728
|
+
async function loadImage(src) {
|
|
1729
|
+
const image = new Image();
|
|
1730
|
+
image.crossOrigin = "anonymous";
|
|
1731
|
+
image.src = src;
|
|
1732
|
+
await new Promise((resolve, reject) => {
|
|
1733
|
+
image.onload = () => resolve();
|
|
1734
|
+
image.onerror = reject;
|
|
1735
|
+
});
|
|
1736
|
+
return image;
|
|
1737
|
+
}
|
|
1738
|
+
async function cropImageWithBlurFill(imageSrc, pixelCrop) {
|
|
1739
|
+
const image = await loadImage(imageSrc);
|
|
1740
|
+
const canvas = document.createElement("canvas");
|
|
1741
|
+
canvas.width = pixelCrop.width;
|
|
1742
|
+
canvas.height = pixelCrop.height;
|
|
1743
|
+
const ctx = canvas.getContext("2d");
|
|
1744
|
+
const extendsLeft = pixelCrop.x < 0;
|
|
1745
|
+
const extendsTop = pixelCrop.y < 0;
|
|
1746
|
+
const extendsRight = pixelCrop.x + pixelCrop.width > image.naturalWidth;
|
|
1747
|
+
const extendsBottom = pixelCrop.y + pixelCrop.height > image.naturalHeight;
|
|
1748
|
+
const needsBlurFill = extendsLeft || extendsTop || extendsRight || extendsBottom;
|
|
1749
|
+
if (needsBlurFill) {
|
|
1750
|
+
const blurCanvas = document.createElement("canvas");
|
|
1751
|
+
blurCanvas.width = pixelCrop.width;
|
|
1752
|
+
blurCanvas.height = pixelCrop.height;
|
|
1753
|
+
const blurCtx = blurCanvas.getContext("2d");
|
|
1754
|
+
blurCtx.filter = "blur(30px) brightness(0.7)";
|
|
1755
|
+
blurCtx.drawImage(image, 0, 0, pixelCrop.width, pixelCrop.height);
|
|
1756
|
+
blurCtx.filter = "none";
|
|
1757
|
+
ctx.drawImage(blurCanvas, 0, 0);
|
|
1758
|
+
const imgX = -pixelCrop.x;
|
|
1759
|
+
const imgY = -pixelCrop.y;
|
|
1760
|
+
ctx.drawImage(image, imgX, imgY, image.naturalWidth, image.naturalHeight);
|
|
1761
|
+
} else {
|
|
1762
|
+
ctx.drawImage(
|
|
1763
|
+
image,
|
|
1764
|
+
pixelCrop.x,
|
|
1765
|
+
pixelCrop.y,
|
|
1766
|
+
pixelCrop.width,
|
|
1767
|
+
pixelCrop.height,
|
|
1768
|
+
0,
|
|
1769
|
+
0,
|
|
1770
|
+
pixelCrop.width,
|
|
1771
|
+
pixelCrop.height
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1774
|
+
return new Promise((resolve) => {
|
|
1775
|
+
canvas.toBlob((blob) => resolve(blob), "image/jpeg", 0.92);
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
function ImageEditModal({
|
|
1779
|
+
src,
|
|
1780
|
+
originalSrc,
|
|
1781
|
+
onSave,
|
|
1782
|
+
onRestore,
|
|
1783
|
+
onClose
|
|
1784
|
+
}) {
|
|
1785
|
+
const actions = useGavaActions();
|
|
1786
|
+
const { strings } = useGavaConfig();
|
|
1787
|
+
const [crop, setCrop] = useState7({ x: 0, y: 0 });
|
|
1788
|
+
const [zoom, setZoom] = useState7(1);
|
|
1789
|
+
const [aspect, setAspect] = useState7(void 0);
|
|
1790
|
+
const [croppedAreaPixels, setCroppedAreaPixels] = useState7(null);
|
|
1791
|
+
const [saving, setSaving] = useState7(false);
|
|
1792
|
+
const onCropComplete = useCallback5((_, pixels) => {
|
|
1793
|
+
setCroppedAreaPixels(pixels);
|
|
1794
|
+
}, []);
|
|
1795
|
+
const handleSave = async () => {
|
|
1796
|
+
if (!croppedAreaPixels) return;
|
|
1797
|
+
setSaving(true);
|
|
1798
|
+
try {
|
|
1799
|
+
const blob = await cropImageWithBlurFill(src, croppedAreaPixels);
|
|
1800
|
+
const formData = new FormData();
|
|
1801
|
+
formData.append("file", blob, "cropped.jpg");
|
|
1802
|
+
const res = await fetch(actions.uploadUrl, {
|
|
1803
|
+
method: "POST",
|
|
1804
|
+
body: formData
|
|
1805
|
+
});
|
|
1806
|
+
const data = await res.json();
|
|
1807
|
+
if (data.url) {
|
|
1808
|
+
onSave(data.url);
|
|
1809
|
+
}
|
|
1810
|
+
} catch {
|
|
1811
|
+
alert(strings.saveError);
|
|
1812
|
+
} finally {
|
|
1813
|
+
setSaving(false);
|
|
1814
|
+
}
|
|
1815
|
+
};
|
|
1816
|
+
return /* @__PURE__ */ jsxs7("div", { className: "fixed inset-0 z-50 flex flex-col bg-black/90", children: [
|
|
1817
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between px-4 py-3 bg-black/50", children: [
|
|
1818
|
+
/* @__PURE__ */ jsx9("h3", { className: "text-white font-medium text-sm", children: strings.editImage }),
|
|
1819
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", children: [
|
|
1820
|
+
originalSrc && onRestore && /* @__PURE__ */ jsxs7(
|
|
1821
|
+
"button",
|
|
1822
|
+
{
|
|
1823
|
+
onClick: () => {
|
|
1824
|
+
onRestore();
|
|
1825
|
+
onClose();
|
|
1826
|
+
},
|
|
1827
|
+
className: "flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-amber-400 hover:text-amber-300 border border-amber-400/30 rounded-full hover:bg-amber-400/10 transition-colors",
|
|
1828
|
+
children: [
|
|
1829
|
+
/* @__PURE__ */ jsx9(RotateCcw2, { className: "w-3.5 h-3.5" }),
|
|
1830
|
+
strings.restoreOriginal
|
|
1831
|
+
]
|
|
1832
|
+
}
|
|
1833
|
+
),
|
|
1834
|
+
/* @__PURE__ */ jsx9(
|
|
1835
|
+
"button",
|
|
1836
|
+
{
|
|
1837
|
+
onClick: onClose,
|
|
1838
|
+
className: "text-white/70 hover:text-white p-1 rounded-full hover:bg-white/10 transition-colors",
|
|
1839
|
+
children: /* @__PURE__ */ jsx9(X5, { className: "w-5 h-5" })
|
|
1840
|
+
}
|
|
1841
|
+
)
|
|
1842
|
+
] })
|
|
1843
|
+
] }),
|
|
1844
|
+
/* @__PURE__ */ jsx9("div", { className: "flex-1 relative", children: /* @__PURE__ */ jsx9(
|
|
1845
|
+
Cropper,
|
|
1846
|
+
{
|
|
1847
|
+
image: src,
|
|
1848
|
+
crop,
|
|
1849
|
+
zoom,
|
|
1850
|
+
minZoom: 0.3,
|
|
1851
|
+
maxZoom: 3,
|
|
1852
|
+
aspect,
|
|
1853
|
+
restrictPosition: false,
|
|
1854
|
+
onCropChange: setCrop,
|
|
1855
|
+
onZoomChange: setZoom,
|
|
1856
|
+
onCropComplete,
|
|
1857
|
+
showGrid: true,
|
|
1858
|
+
style: {
|
|
1859
|
+
containerStyle: { background: "transparent" }
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
) }),
|
|
1863
|
+
/* @__PURE__ */ jsxs7("div", { className: "bg-black/70 backdrop-blur-sm px-4 py-4 space-y-3", children: [
|
|
1864
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-3 justify-center", children: [
|
|
1865
|
+
/* @__PURE__ */ jsx9(
|
|
1866
|
+
"button",
|
|
1867
|
+
{
|
|
1868
|
+
onClick: () => setZoom(Math.max(0.3, zoom - 0.2)),
|
|
1869
|
+
className: "text-white/70 hover:text-white p-1.5 rounded-full hover:bg-white/10 transition-colors",
|
|
1870
|
+
children: /* @__PURE__ */ jsx9(ZoomOut, { className: "w-4 h-4" })
|
|
1871
|
+
}
|
|
1872
|
+
),
|
|
1873
|
+
/* @__PURE__ */ jsx9(
|
|
1874
|
+
"input",
|
|
1875
|
+
{
|
|
1876
|
+
type: "range",
|
|
1877
|
+
min: 0.3,
|
|
1878
|
+
max: 3,
|
|
1879
|
+
step: 0.05,
|
|
1880
|
+
value: zoom,
|
|
1881
|
+
onChange: (e) => setZoom(Number(e.target.value)),
|
|
1882
|
+
className: "w-48 accent-blue-500"
|
|
1883
|
+
}
|
|
1884
|
+
),
|
|
1885
|
+
/* @__PURE__ */ jsx9(
|
|
1886
|
+
"button",
|
|
1887
|
+
{
|
|
1888
|
+
onClick: () => setZoom(Math.min(3, zoom + 0.2)),
|
|
1889
|
+
className: "text-white/70 hover:text-white p-1.5 rounded-full hover:bg-white/10 transition-colors",
|
|
1890
|
+
children: /* @__PURE__ */ jsx9(ZoomIn, { className: "w-4 h-4" })
|
|
1891
|
+
}
|
|
1892
|
+
),
|
|
1893
|
+
/* @__PURE__ */ jsxs7("span", { className: "text-white/50 text-xs w-12 text-center", children: [
|
|
1894
|
+
Math.round(zoom * 100),
|
|
1895
|
+
"%"
|
|
1896
|
+
] })
|
|
1897
|
+
] }),
|
|
1898
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between", children: [
|
|
1899
|
+
/* @__PURE__ */ jsx9("div", { className: "flex gap-1", children: ASPECT_PRESETS.map((p) => {
|
|
1900
|
+
const Icon = p.icon;
|
|
1901
|
+
const isActive = aspect === p.value || aspect === void 0 && p.value === void 0;
|
|
1902
|
+
return /* @__PURE__ */ jsxs7(
|
|
1903
|
+
"button",
|
|
1904
|
+
{
|
|
1905
|
+
onClick: () => setAspect(p.value),
|
|
1906
|
+
className: `flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition-colors ${isActive ? "bg-blue-500 text-white" : "text-white/60 hover:text-white hover:bg-white/10"}`,
|
|
1907
|
+
children: [
|
|
1908
|
+
/* @__PURE__ */ jsx9(Icon, { className: "w-3.5 h-3.5" }),
|
|
1909
|
+
p.label
|
|
1910
|
+
]
|
|
1911
|
+
},
|
|
1912
|
+
p.label
|
|
1913
|
+
);
|
|
1914
|
+
}) }),
|
|
1915
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex gap-2", children: [
|
|
1916
|
+
/* @__PURE__ */ jsx9(
|
|
1917
|
+
"button",
|
|
1918
|
+
{
|
|
1919
|
+
onClick: onClose,
|
|
1920
|
+
className: "px-4 py-1.5 text-sm text-white/70 hover:text-white border border-white/20 rounded-full hover:bg-white/10 transition-colors",
|
|
1921
|
+
children: strings.cancel
|
|
1922
|
+
}
|
|
1923
|
+
),
|
|
1924
|
+
/* @__PURE__ */ jsxs7(
|
|
1925
|
+
"button",
|
|
1926
|
+
{
|
|
1927
|
+
onClick: handleSave,
|
|
1928
|
+
disabled: saving,
|
|
1929
|
+
className: "px-4 py-1.5 text-sm bg-blue-500 text-white rounded-full hover:bg-blue-600 transition-colors font-medium flex items-center gap-1.5 disabled:opacity-50",
|
|
1930
|
+
children: [
|
|
1931
|
+
saving ? /* @__PURE__ */ jsx9(Loader23, { className: "w-3.5 h-3.5 animate-spin" }) : /* @__PURE__ */ jsx9(Check2, { className: "w-3.5 h-3.5" }),
|
|
1932
|
+
strings.apply
|
|
1933
|
+
]
|
|
1934
|
+
}
|
|
1935
|
+
)
|
|
1936
|
+
] })
|
|
1937
|
+
] })
|
|
1938
|
+
] })
|
|
1939
|
+
] });
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// src/components/articles/ArticleList.tsx
|
|
1943
|
+
import { useState as useState8, useRef as useRef4, useEffect as useEffect5, useCallback as useCallback6 } from "react";
|
|
1944
|
+
import Link2 from "next/link";
|
|
1945
|
+
import { useRouter as useRouter2 } from "next/navigation";
|
|
1946
|
+
import {
|
|
1947
|
+
Plus as Plus2,
|
|
1948
|
+
FileText as FileText2,
|
|
1949
|
+
Globe as Globe2,
|
|
1950
|
+
Pencil,
|
|
1951
|
+
Trash2 as Trash22,
|
|
1952
|
+
Search as Search2,
|
|
1953
|
+
EyeOff
|
|
1954
|
+
} from "lucide-react";
|
|
1955
|
+
import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1956
|
+
function ArticleList({
|
|
1957
|
+
articles,
|
|
1958
|
+
canCreate,
|
|
1959
|
+
canEdit,
|
|
1960
|
+
canPublish: canPub
|
|
1961
|
+
}) {
|
|
1962
|
+
const router = useRouter2();
|
|
1963
|
+
const actions = useGavaActions();
|
|
1964
|
+
const { strings, routes } = useGavaConfig();
|
|
1965
|
+
const filterLabels = {
|
|
1966
|
+
tutti: strings.all,
|
|
1967
|
+
bozza: strings.drafts,
|
|
1968
|
+
pubblicato: strings.published
|
|
1969
|
+
};
|
|
1970
|
+
const [filter, setFilter] = useState8("tutti");
|
|
1971
|
+
const [search, setSearch] = useState8("");
|
|
1972
|
+
const filterRefs = useRef4({});
|
|
1973
|
+
const [indicator, setIndicator] = useState8({ left: 0, width: 0 });
|
|
1974
|
+
const filtered = articles.filter((a) => {
|
|
1975
|
+
if (filter !== "tutti" && a.status !== filter) return false;
|
|
1976
|
+
if (search && !a.title.toLowerCase().includes(search.toLowerCase()))
|
|
1977
|
+
return false;
|
|
1978
|
+
return true;
|
|
1979
|
+
});
|
|
1980
|
+
const counts = {
|
|
1981
|
+
tutti: articles.length,
|
|
1982
|
+
bozza: articles.filter((a) => a.status === "bozza").length,
|
|
1983
|
+
pubblicato: articles.filter((a) => a.status === "pubblicato").length
|
|
1984
|
+
};
|
|
1985
|
+
const updateIndicator = useCallback6(() => {
|
|
1986
|
+
const el = filterRefs.current[filter];
|
|
1987
|
+
if (el) {
|
|
1988
|
+
setIndicator({
|
|
1989
|
+
left: el.offsetLeft,
|
|
1990
|
+
width: el.offsetWidth
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
}, [filter]);
|
|
1994
|
+
useEffect5(() => {
|
|
1995
|
+
updateIndicator();
|
|
1996
|
+
window.addEventListener("resize", updateIndicator);
|
|
1997
|
+
return () => window.removeEventListener("resize", updateIndicator);
|
|
1998
|
+
}, [updateIndicator]);
|
|
1999
|
+
const handleDelete = async (id, title) => {
|
|
2000
|
+
if (!confirm(strings.deleteConfirm(title))) return;
|
|
2001
|
+
await actions.deleteArticle(id);
|
|
2002
|
+
router.refresh();
|
|
2003
|
+
};
|
|
2004
|
+
const handleUnpublish = async (id, title) => {
|
|
2005
|
+
if (!confirm(strings.unpublishConfirm(title))) return;
|
|
2006
|
+
await actions.unpublishArticle(id);
|
|
2007
|
+
router.refresh();
|
|
2008
|
+
};
|
|
2009
|
+
return /* @__PURE__ */ jsxs8("div", { className: "dash-animate-in", children: [
|
|
2010
|
+
/* @__PURE__ */ jsxs8("div", { className: "flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6", children: [
|
|
2011
|
+
/* @__PURE__ */ jsxs8("div", { children: [
|
|
2012
|
+
/* @__PURE__ */ jsx10("h1", { className: "text-2xl font-bold text-foreground mb-1", children: strings.articles }),
|
|
2013
|
+
/* @__PURE__ */ jsx10("p", { className: "text-sm text-muted", children: strings.totalArticles(articles.length) })
|
|
2014
|
+
] }),
|
|
2015
|
+
canCreate && /* @__PURE__ */ jsxs8(
|
|
2016
|
+
Link2,
|
|
2017
|
+
{
|
|
2018
|
+
href: routes.articleNew,
|
|
2019
|
+
className: "inline-flex items-center gap-2 px-4 py-2.5 bg-accent text-white rounded-full hover:bg-accent-hover transition-all duration-200 text-sm font-medium hover:shadow-md active:scale-[0.97]",
|
|
2020
|
+
children: [
|
|
2021
|
+
/* @__PURE__ */ jsx10(Plus2, { className: "w-4 h-4" }),
|
|
2022
|
+
strings.newArticle
|
|
2023
|
+
]
|
|
2024
|
+
}
|
|
2025
|
+
)
|
|
2026
|
+
] }),
|
|
2027
|
+
/* @__PURE__ */ jsxs8("div", { className: "flex flex-col sm:flex-row gap-3 mb-6", children: [
|
|
2028
|
+
/* @__PURE__ */ jsxs8("div", { className: "dash-filter-group flex bg-card border border-card-border rounded-full p-1 gap-1", children: [
|
|
2029
|
+
/* @__PURE__ */ jsx10(
|
|
2030
|
+
"div",
|
|
2031
|
+
{
|
|
2032
|
+
className: "dash-filter-indicator",
|
|
2033
|
+
style: { left: indicator.left, width: indicator.width }
|
|
2034
|
+
}
|
|
2035
|
+
),
|
|
2036
|
+
["tutti", "bozza", "pubblicato"].map((f) => /* @__PURE__ */ jsxs8(
|
|
2037
|
+
"button",
|
|
2038
|
+
{
|
|
2039
|
+
ref: (el) => {
|
|
2040
|
+
filterRefs.current[f] = el;
|
|
2041
|
+
},
|
|
2042
|
+
onClick: () => setFilter(f),
|
|
2043
|
+
className: `dash-filter-btn px-3 py-1.5 text-sm rounded-full capitalize ${filter === f ? "text-white" : "text-muted hover:text-foreground"}`,
|
|
2044
|
+
children: [
|
|
2045
|
+
filterLabels[f],
|
|
2046
|
+
" ",
|
|
2047
|
+
/* @__PURE__ */ jsxs8(
|
|
2048
|
+
"span",
|
|
2049
|
+
{
|
|
2050
|
+
className: `text-xs ${filter === f ? "opacity-80" : "opacity-50"}`,
|
|
2051
|
+
children: [
|
|
2052
|
+
"(",
|
|
2053
|
+
counts[f],
|
|
2054
|
+
")"
|
|
2055
|
+
]
|
|
2056
|
+
}
|
|
2057
|
+
)
|
|
2058
|
+
]
|
|
2059
|
+
},
|
|
2060
|
+
f
|
|
2061
|
+
))
|
|
2062
|
+
] }),
|
|
2063
|
+
/* @__PURE__ */ jsxs8("div", { className: "relative flex-1 max-w-xs", children: [
|
|
2064
|
+
/* @__PURE__ */ jsx10(Search2, { className: "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted" }),
|
|
2065
|
+
/* @__PURE__ */ jsx10(
|
|
2066
|
+
"input",
|
|
2067
|
+
{
|
|
2068
|
+
type: "text",
|
|
2069
|
+
value: search,
|
|
2070
|
+
onChange: (e) => setSearch(e.target.value),
|
|
2071
|
+
placeholder: strings.searchArticles,
|
|
2072
|
+
className: "dash-search w-full pl-9 pr-3 py-2 text-sm bg-card border border-card-border rounded-full outline-none focus:border-accent"
|
|
2073
|
+
}
|
|
2074
|
+
)
|
|
2075
|
+
] })
|
|
2076
|
+
] }),
|
|
2077
|
+
filtered.length === 0 ? /* @__PURE__ */ jsxs8("div", { className: "dash-scale-in bg-card border border-card-border rounded-xl p-12 text-center", children: [
|
|
2078
|
+
/* @__PURE__ */ jsx10(FileText2, { className: "dash-empty-icon w-10 h-10 text-muted mx-auto mb-3" }),
|
|
2079
|
+
/* @__PURE__ */ jsx10("p", { className: "text-muted text-sm", children: search ? strings.noArticlesSearch : strings.noArticles })
|
|
2080
|
+
] }) : /* @__PURE__ */ jsx10("div", { className: "bg-card border border-card-border rounded-xl overflow-hidden", children: /* @__PURE__ */ jsx10("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs8("table", { className: "w-full text-sm", children: [
|
|
2081
|
+
/* @__PURE__ */ jsx10("thead", { children: /* @__PURE__ */ jsxs8("tr", { className: "border-b border-card-border text-left", children: [
|
|
2082
|
+
/* @__PURE__ */ jsx10("th", { className: "px-4 py-3 font-medium text-muted", children: strings.title }),
|
|
2083
|
+
/* @__PURE__ */ jsx10("th", { className: "px-4 py-3 font-medium text-muted hidden md:table-cell", children: strings.author }),
|
|
2084
|
+
/* @__PURE__ */ jsx10("th", { className: "px-4 py-3 font-medium text-muted hidden lg:table-cell", children: strings.category }),
|
|
2085
|
+
/* @__PURE__ */ jsx10("th", { className: "px-4 py-3 font-medium text-muted", children: strings.status }),
|
|
2086
|
+
/* @__PURE__ */ jsx10("th", { className: "px-4 py-3 font-medium text-muted hidden sm:table-cell", children: strings.updated }),
|
|
2087
|
+
canEdit && /* @__PURE__ */ jsx10("th", { className: "px-4 py-3 font-medium text-muted text-right", children: strings.actions })
|
|
2088
|
+
] }) }),
|
|
2089
|
+
/* @__PURE__ */ jsx10("tbody", { children: filtered.map((article) => /* @__PURE__ */ jsxs8(
|
|
2090
|
+
"tr",
|
|
2091
|
+
{
|
|
2092
|
+
className: "border-b border-card-border/50 last:border-0 hover:bg-card-border/20 transition-colors duration-150",
|
|
2093
|
+
children: [
|
|
2094
|
+
/* @__PURE__ */ jsx10("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-3", children: [
|
|
2095
|
+
article.coverImage ? /* @__PURE__ */ jsx10(
|
|
2096
|
+
"img",
|
|
2097
|
+
{
|
|
2098
|
+
src: article.coverImage,
|
|
2099
|
+
alt: "",
|
|
2100
|
+
className: "w-10 h-10 rounded-md object-cover flex-shrink-0 bg-card-border transition-transform duration-200 hover:scale-105"
|
|
2101
|
+
}
|
|
2102
|
+
) : /* @__PURE__ */ jsx10("div", { className: "w-10 h-10 rounded-md bg-card-border flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ jsx10(FileText2, { className: "w-4 h-4 text-muted" }) }),
|
|
2103
|
+
/* @__PURE__ */ jsx10("span", { className: "font-medium text-foreground", children: article.title || /* @__PURE__ */ jsx10("span", { className: "italic text-muted", children: strings.untitled }) })
|
|
2104
|
+
] }) }),
|
|
2105
|
+
/* @__PURE__ */ jsx10("td", { className: "px-4 py-3 text-muted hidden md:table-cell", children: article.authorName || "\u2014" }),
|
|
2106
|
+
/* @__PURE__ */ jsx10("td", { className: "px-4 py-3 text-muted hidden lg:table-cell", children: article.category || "\u2014" }),
|
|
2107
|
+
/* @__PURE__ */ jsx10("td", { className: "px-4 py-3", children: article.status === "pubblicato" ? /* @__PURE__ */ jsxs8("span", { className: "inline-flex items-center gap-1 text-xs font-medium text-green-600 bg-green-50 px-2 py-0.5 rounded-full", children: [
|
|
2108
|
+
/* @__PURE__ */ jsx10(Globe2, { className: "w-3 h-3" }),
|
|
2109
|
+
strings.published
|
|
2110
|
+
] }) : /* @__PURE__ */ jsxs8("span", { className: "inline-flex items-center gap-1 text-xs font-medium text-amber-600 bg-amber-50 px-2 py-0.5 rounded-full", children: [
|
|
2111
|
+
/* @__PURE__ */ jsx10(FileText2, { className: "w-3 h-3" }),
|
|
2112
|
+
strings.draft
|
|
2113
|
+
] }) }),
|
|
2114
|
+
/* @__PURE__ */ jsx10("td", { className: "px-4 py-3 text-muted text-sm hidden sm:table-cell", children: new Date(article.updatedAt).toLocaleDateString("it-IT", {
|
|
2115
|
+
day: "numeric",
|
|
2116
|
+
month: "short",
|
|
2117
|
+
year: "numeric"
|
|
2118
|
+
}) }),
|
|
2119
|
+
canEdit && /* @__PURE__ */ jsx10("td", { className: "px-4 py-3 text-right", children: /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-end gap-1", children: [
|
|
2120
|
+
canPub && article.status === "pubblicato" && /* @__PURE__ */ jsx10(
|
|
2121
|
+
"button",
|
|
2122
|
+
{
|
|
2123
|
+
onClick: () => handleUnpublish(article.id, article.title),
|
|
2124
|
+
className: "p-1.5 rounded-full text-muted hover:text-amber-600 hover:bg-amber-50 transition-all duration-200 active:scale-90",
|
|
2125
|
+
title: strings.unpublish,
|
|
2126
|
+
children: /* @__PURE__ */ jsx10(EyeOff, { className: "w-4 h-4" })
|
|
2127
|
+
}
|
|
2128
|
+
),
|
|
2129
|
+
/* @__PURE__ */ jsx10(
|
|
2130
|
+
Link2,
|
|
2131
|
+
{
|
|
2132
|
+
href: routes.articleEdit(article.id),
|
|
2133
|
+
className: "p-1.5 rounded-full text-muted hover:text-foreground hover:bg-card-border/50 transition-all duration-200 active:scale-90",
|
|
2134
|
+
title: strings.edit,
|
|
2135
|
+
children: /* @__PURE__ */ jsx10(Pencil, { className: "w-4 h-4" })
|
|
2136
|
+
}
|
|
2137
|
+
),
|
|
2138
|
+
/* @__PURE__ */ jsx10(
|
|
2139
|
+
"button",
|
|
2140
|
+
{
|
|
2141
|
+
onClick: () => handleDelete(article.id, article.title),
|
|
2142
|
+
className: "p-1.5 rounded-full text-muted hover:text-red-500 hover:bg-red-50 transition-all duration-200 active:scale-90",
|
|
2143
|
+
title: strings.delete,
|
|
2144
|
+
children: /* @__PURE__ */ jsx10(Trash22, { className: "w-4 h-4" })
|
|
2145
|
+
}
|
|
2146
|
+
)
|
|
2147
|
+
] }) })
|
|
2148
|
+
]
|
|
2149
|
+
},
|
|
2150
|
+
article.id
|
|
2151
|
+
)) })
|
|
2152
|
+
] }) }) })
|
|
2153
|
+
] });
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
// src/components/users/UserTable.tsx
|
|
2157
|
+
import Link3 from "next/link";
|
|
2158
|
+
import { Pencil as Pencil2, Trash2 as Trash23 } from "lucide-react";
|
|
2159
|
+
import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2160
|
+
function UserTable({
|
|
2161
|
+
users,
|
|
2162
|
+
currentUserId
|
|
2163
|
+
}) {
|
|
2164
|
+
const actions = useGavaActions();
|
|
2165
|
+
const { strings, roles, routes } = useGavaConfig();
|
|
2166
|
+
async function handleDelete(id, name) {
|
|
2167
|
+
if (!confirm(strings.deleteUserConfirm(name))) return;
|
|
2168
|
+
const result = await actions.deleteUser(id);
|
|
2169
|
+
if (result && "error" in result && result.error) {
|
|
2170
|
+
alert(result.error);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
return /* @__PURE__ */ jsx11("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs9("table", { className: "w-full text-sm", children: [
|
|
2174
|
+
/* @__PURE__ */ jsx11("thead", { children: /* @__PURE__ */ jsxs9("tr", { className: "border-b border-card-border", children: [
|
|
2175
|
+
/* @__PURE__ */ jsx11("th", { className: "text-left py-3 px-4 font-semibold text-foreground", children: strings.name }),
|
|
2176
|
+
/* @__PURE__ */ jsx11("th", { className: "text-left py-3 px-4 font-semibold text-foreground", children: strings.email }),
|
|
2177
|
+
/* @__PURE__ */ jsx11("th", { className: "text-left py-3 px-4 font-semibold text-foreground", children: strings.role }),
|
|
2178
|
+
/* @__PURE__ */ jsx11("th", { className: "text-left py-3 px-4 font-semibold text-foreground", children: strings.createdAt }),
|
|
2179
|
+
/* @__PURE__ */ jsx11("th", { className: "text-right py-3 px-4 font-semibold text-foreground", children: strings.actions })
|
|
2180
|
+
] }) }),
|
|
2181
|
+
/* @__PURE__ */ jsx11("tbody", { children: users.map((user) => /* @__PURE__ */ jsxs9(
|
|
2182
|
+
"tr",
|
|
2183
|
+
{
|
|
2184
|
+
className: "border-b border-card-border hover:bg-card/50",
|
|
2185
|
+
children: [
|
|
2186
|
+
/* @__PURE__ */ jsx11("td", { className: "py-3 px-4 text-foreground", children: user.name }),
|
|
2187
|
+
/* @__PURE__ */ jsx11("td", { className: "py-3 px-4 text-muted", children: user.email }),
|
|
2188
|
+
/* @__PURE__ */ jsx11("td", { className: "py-3 px-4", children: /* @__PURE__ */ jsx11("span", { className: "inline-block px-2 py-0.5 bg-accent/10 text-accent text-xs font-medium rounded-full", children: roles.labels[user.role] || user.role }) }),
|
|
2189
|
+
/* @__PURE__ */ jsx11("td", { className: "py-3 px-4 text-muted", children: new Date(user.createdAt).toLocaleDateString("it-IT") }),
|
|
2190
|
+
/* @__PURE__ */ jsx11("td", { className: "py-3 px-4 text-right", children: /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-end gap-2", children: [
|
|
2191
|
+
/* @__PURE__ */ jsx11(
|
|
2192
|
+
Link3,
|
|
2193
|
+
{
|
|
2194
|
+
href: routes.userEdit(user.id),
|
|
2195
|
+
className: "p-1.5 text-muted hover:text-accent transition-colors",
|
|
2196
|
+
title: strings.edit,
|
|
2197
|
+
children: /* @__PURE__ */ jsx11(Pencil2, { className: "w-4 h-4" })
|
|
2198
|
+
}
|
|
2199
|
+
),
|
|
2200
|
+
user.id !== currentUserId && /* @__PURE__ */ jsx11(
|
|
2201
|
+
"button",
|
|
2202
|
+
{
|
|
2203
|
+
onClick: () => handleDelete(user.id, user.name),
|
|
2204
|
+
className: "p-1.5 text-muted hover:text-red-500 transition-colors",
|
|
2205
|
+
title: strings.delete,
|
|
2206
|
+
children: /* @__PURE__ */ jsx11(Trash23, { className: "w-4 h-4" })
|
|
2207
|
+
}
|
|
2208
|
+
)
|
|
2209
|
+
] }) })
|
|
2210
|
+
]
|
|
2211
|
+
},
|
|
2212
|
+
user.id
|
|
2213
|
+
)) })
|
|
2214
|
+
] }) });
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
// src/components/users/UserForm.tsx
|
|
2218
|
+
import { useState as useState9 } from "react";
|
|
2219
|
+
import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2220
|
+
function UserForm({ user }) {
|
|
2221
|
+
const actions = useGavaActions();
|
|
2222
|
+
const { strings, roles } = useGavaConfig();
|
|
2223
|
+
const [error, setError] = useState9("");
|
|
2224
|
+
const isEditing = !!user;
|
|
2225
|
+
async function handleSubmit(formData) {
|
|
2226
|
+
setError("");
|
|
2227
|
+
let result;
|
|
2228
|
+
if (isEditing) {
|
|
2229
|
+
result = await actions.updateUser(user.id, formData);
|
|
2230
|
+
} else {
|
|
2231
|
+
result = await actions.createUser(formData);
|
|
2232
|
+
}
|
|
2233
|
+
if (result && "error" in result && result.error) {
|
|
2234
|
+
setError(result.error);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
return /* @__PURE__ */ jsxs10("form", { action: handleSubmit, className: "space-y-4 max-w-md", children: [
|
|
2238
|
+
error && /* @__PURE__ */ jsx12("div", { className: "bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-4 py-3 rounded-lg text-sm", children: error }),
|
|
2239
|
+
/* @__PURE__ */ jsxs10("div", { children: [
|
|
2240
|
+
/* @__PURE__ */ jsx12(
|
|
2241
|
+
"label",
|
|
2242
|
+
{
|
|
2243
|
+
htmlFor: "name",
|
|
2244
|
+
className: "block text-sm font-medium text-foreground mb-1",
|
|
2245
|
+
children: strings.name
|
|
2246
|
+
}
|
|
2247
|
+
),
|
|
2248
|
+
/* @__PURE__ */ jsx12(
|
|
2249
|
+
"input",
|
|
2250
|
+
{
|
|
2251
|
+
id: "name",
|
|
2252
|
+
name: "name",
|
|
2253
|
+
type: "text",
|
|
2254
|
+
required: true,
|
|
2255
|
+
defaultValue: user?.name || "",
|
|
2256
|
+
className: "w-full px-3 py-2 bg-background border border-card-border rounded-lg text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
|
|
2257
|
+
}
|
|
2258
|
+
)
|
|
2259
|
+
] }),
|
|
2260
|
+
/* @__PURE__ */ jsxs10("div", { children: [
|
|
2261
|
+
/* @__PURE__ */ jsx12(
|
|
2262
|
+
"label",
|
|
2263
|
+
{
|
|
2264
|
+
htmlFor: "email",
|
|
2265
|
+
className: "block text-sm font-medium text-foreground mb-1",
|
|
2266
|
+
children: strings.email
|
|
2267
|
+
}
|
|
2268
|
+
),
|
|
2269
|
+
/* @__PURE__ */ jsx12(
|
|
2270
|
+
"input",
|
|
2271
|
+
{
|
|
2272
|
+
id: "email",
|
|
2273
|
+
name: "email",
|
|
2274
|
+
type: "email",
|
|
2275
|
+
required: true,
|
|
2276
|
+
defaultValue: user?.email || "",
|
|
2277
|
+
className: "w-full px-3 py-2 bg-background border border-card-border rounded-lg text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
|
|
2278
|
+
}
|
|
2279
|
+
)
|
|
2280
|
+
] }),
|
|
2281
|
+
/* @__PURE__ */ jsxs10("div", { children: [
|
|
2282
|
+
/* @__PURE__ */ jsxs10(
|
|
2283
|
+
"label",
|
|
2284
|
+
{
|
|
2285
|
+
htmlFor: "password",
|
|
2286
|
+
className: "block text-sm font-medium text-foreground mb-1",
|
|
2287
|
+
children: [
|
|
2288
|
+
strings.password,
|
|
2289
|
+
isEditing && strings.passwordEditHint
|
|
2290
|
+
]
|
|
2291
|
+
}
|
|
2292
|
+
),
|
|
2293
|
+
/* @__PURE__ */ jsx12(
|
|
2294
|
+
"input",
|
|
2295
|
+
{
|
|
2296
|
+
id: "password",
|
|
2297
|
+
name: "password",
|
|
2298
|
+
type: "password",
|
|
2299
|
+
required: !isEditing,
|
|
2300
|
+
minLength: 6,
|
|
2301
|
+
className: "w-full px-3 py-2 bg-background border border-card-border rounded-lg text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent",
|
|
2302
|
+
placeholder: isEditing ? "" : "Min 6 chars"
|
|
2303
|
+
}
|
|
2304
|
+
)
|
|
2305
|
+
] }),
|
|
2306
|
+
/* @__PURE__ */ jsxs10("div", { children: [
|
|
2307
|
+
/* @__PURE__ */ jsx12(
|
|
2308
|
+
"label",
|
|
2309
|
+
{
|
|
2310
|
+
htmlFor: "role",
|
|
2311
|
+
className: "block text-sm font-medium text-foreground mb-1",
|
|
2312
|
+
children: strings.role
|
|
2313
|
+
}
|
|
2314
|
+
),
|
|
2315
|
+
/* @__PURE__ */ jsx12(
|
|
2316
|
+
"select",
|
|
2317
|
+
{
|
|
2318
|
+
id: "role",
|
|
2319
|
+
name: "role",
|
|
2320
|
+
required: true,
|
|
2321
|
+
defaultValue: user?.role || roles.list[roles.list.length - 1],
|
|
2322
|
+
className: "w-full px-3 py-2 bg-background border border-card-border rounded-lg text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent",
|
|
2323
|
+
children: roles.list.map((r) => /* @__PURE__ */ jsx12("option", { value: r, children: roles.labels[r] || r }, r))
|
|
2324
|
+
}
|
|
2325
|
+
)
|
|
2326
|
+
] }),
|
|
2327
|
+
/* @__PURE__ */ jsx12(
|
|
2328
|
+
"button",
|
|
2329
|
+
{
|
|
2330
|
+
type: "submit",
|
|
2331
|
+
className: "px-6 py-2.5 bg-accent text-white rounded-full font-medium text-sm hover:bg-accent-hover transition-colors",
|
|
2332
|
+
children: isEditing ? strings.saveChanges : strings.createUser
|
|
2333
|
+
}
|
|
2334
|
+
)
|
|
2335
|
+
] });
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
// src/components/media/MediaGrid.tsx
|
|
2339
|
+
import { useState as useState10 } from "react";
|
|
2340
|
+
import { useRouter as useRouter3 } from "next/navigation";
|
|
2341
|
+
import { Search as Search3, Trash2 as Trash24, Copy, Check as Check3, ImageIcon as ImageIcon2 } from "lucide-react";
|
|
2342
|
+
import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2343
|
+
function formatSize2(bytes) {
|
|
2344
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
2345
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
2346
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
2347
|
+
}
|
|
2348
|
+
function MediaGrid({ media }) {
|
|
2349
|
+
const router = useRouter3();
|
|
2350
|
+
const actions = useGavaActions();
|
|
2351
|
+
const { strings } = useGavaConfig();
|
|
2352
|
+
const [search, setSearch] = useState10("");
|
|
2353
|
+
const [copiedId, setCopiedId] = useState10(null);
|
|
2354
|
+
const filtered = media.filter(
|
|
2355
|
+
(m) => search ? m.filename.toLowerCase().includes(search.toLowerCase()) : true
|
|
2356
|
+
);
|
|
2357
|
+
const handleCopy = async (path, id) => {
|
|
2358
|
+
await navigator.clipboard.writeText(path);
|
|
2359
|
+
setCopiedId(id);
|
|
2360
|
+
setTimeout(() => setCopiedId(null), 2e3);
|
|
2361
|
+
};
|
|
2362
|
+
const handleDelete = async (id, filename) => {
|
|
2363
|
+
if (!confirm(strings.deleteConfirm(filename))) return;
|
|
2364
|
+
try {
|
|
2365
|
+
await actions.deleteMedia(id);
|
|
2366
|
+
router.refresh();
|
|
2367
|
+
} catch (err) {
|
|
2368
|
+
alert(
|
|
2369
|
+
err instanceof Error ? err.message : "Error deleting file"
|
|
2370
|
+
);
|
|
2371
|
+
}
|
|
2372
|
+
};
|
|
2373
|
+
return /* @__PURE__ */ jsxs11("div", { className: "dash-animate-in", children: [
|
|
2374
|
+
/* @__PURE__ */ jsxs11("div", { className: "flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6", children: [
|
|
2375
|
+
/* @__PURE__ */ jsxs11("div", { children: [
|
|
2376
|
+
/* @__PURE__ */ jsx13("h1", { className: "text-2xl font-bold text-foreground mb-1", children: strings.media }),
|
|
2377
|
+
/* @__PURE__ */ jsx13("p", { className: "text-sm text-muted", children: strings.totalFiles(media.length) })
|
|
2378
|
+
] }),
|
|
2379
|
+
/* @__PURE__ */ jsxs11("div", { className: "relative max-w-xs w-full", children: [
|
|
2380
|
+
/* @__PURE__ */ jsx13(Search3, { className: "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted" }),
|
|
2381
|
+
/* @__PURE__ */ jsx13(
|
|
2382
|
+
"input",
|
|
2383
|
+
{
|
|
2384
|
+
type: "text",
|
|
2385
|
+
value: search,
|
|
2386
|
+
onChange: (e) => setSearch(e.target.value),
|
|
2387
|
+
placeholder: strings.searchFiles,
|
|
2388
|
+
className: "dash-search w-full pl-9 pr-3 py-2 text-sm bg-card border border-card-border rounded-full outline-none focus:border-accent"
|
|
2389
|
+
}
|
|
2390
|
+
)
|
|
2391
|
+
] })
|
|
2392
|
+
] }),
|
|
2393
|
+
filtered.length === 0 ? /* @__PURE__ */ jsxs11("div", { className: "dash-scale-in bg-card border border-card-border rounded-xl p-12 text-center", children: [
|
|
2394
|
+
/* @__PURE__ */ jsx13(ImageIcon2, { className: "dash-empty-icon w-10 h-10 text-muted mx-auto mb-3" }),
|
|
2395
|
+
/* @__PURE__ */ jsx13("p", { className: "text-muted text-sm", children: search ? strings.noMediaSearch : strings.noMedia })
|
|
2396
|
+
] }) : /* @__PURE__ */ jsx13("div", { className: "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4", children: filtered.map((m, i) => /* @__PURE__ */ jsxs11(
|
|
2397
|
+
"div",
|
|
2398
|
+
{
|
|
2399
|
+
className: "dash-animate-in dash-stagger dash-card-hover bg-card border border-card-border rounded-xl overflow-hidden group",
|
|
2400
|
+
style: { "--i": i },
|
|
2401
|
+
children: [
|
|
2402
|
+
/* @__PURE__ */ jsxs11("div", { className: "aspect-square bg-card-border relative overflow-hidden", children: [
|
|
2403
|
+
m.mimeType.startsWith("image/") ? /* @__PURE__ */ jsx13(
|
|
2404
|
+
"img",
|
|
2405
|
+
{
|
|
2406
|
+
src: m.path,
|
|
2407
|
+
alt: m.filename,
|
|
2408
|
+
className: "dash-media-img w-full h-full object-cover"
|
|
2409
|
+
}
|
|
2410
|
+
) : /* @__PURE__ */ jsx13("div", { className: "w-full h-full flex items-center justify-center", children: /* @__PURE__ */ jsx13(ImageIcon2, { className: "w-8 h-8 text-muted" }) }),
|
|
2411
|
+
/* @__PURE__ */ jsxs11("div", { className: "absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center justify-center gap-2", children: [
|
|
2412
|
+
/* @__PURE__ */ jsx13(
|
|
2413
|
+
"button",
|
|
2414
|
+
{
|
|
2415
|
+
onClick: () => handleCopy(m.path, m.id),
|
|
2416
|
+
className: "p-2 rounded-full bg-white/90 text-foreground hover:bg-white transition-all duration-200 active:scale-90",
|
|
2417
|
+
title: strings.copyUrl,
|
|
2418
|
+
children: copiedId === m.id ? /* @__PURE__ */ jsx13(Check3, { className: "w-4 h-4 text-green-600" }) : /* @__PURE__ */ jsx13(Copy, { className: "w-4 h-4" })
|
|
2419
|
+
}
|
|
2420
|
+
),
|
|
2421
|
+
/* @__PURE__ */ jsx13(
|
|
2422
|
+
"button",
|
|
2423
|
+
{
|
|
2424
|
+
onClick: () => handleDelete(m.id, m.filename),
|
|
2425
|
+
className: "p-2 rounded-full bg-white/90 text-red-500 hover:bg-white transition-all duration-200 active:scale-90",
|
|
2426
|
+
title: strings.delete,
|
|
2427
|
+
children: /* @__PURE__ */ jsx13(Trash24, { className: "w-4 h-4" })
|
|
2428
|
+
}
|
|
2429
|
+
)
|
|
2430
|
+
] })
|
|
2431
|
+
] }),
|
|
2432
|
+
/* @__PURE__ */ jsxs11("div", { className: "p-2.5", children: [
|
|
2433
|
+
/* @__PURE__ */ jsx13("p", { className: "text-xs font-medium text-foreground truncate", children: m.filename }),
|
|
2434
|
+
/* @__PURE__ */ jsxs11("p", { className: "text-[10px] text-muted", children: [
|
|
2435
|
+
formatSize2(m.size),
|
|
2436
|
+
" \u2014 ",
|
|
2437
|
+
m.uploader.name
|
|
2438
|
+
] })
|
|
2439
|
+
] })
|
|
2440
|
+
]
|
|
2441
|
+
},
|
|
2442
|
+
m.id
|
|
2443
|
+
)) })
|
|
2444
|
+
] });
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
// src/components/dashboard/DashboardNavbar.tsx
|
|
2448
|
+
import Link4 from "next/link";
|
|
2449
|
+
import { usePathname } from "next/navigation";
|
|
2450
|
+
import { useSession, signOut } from "next-auth/react";
|
|
2451
|
+
import { FileText as FileText3, Users, LogOut, BarChart3, ImageIcon as ImageIcon3 } from "lucide-react";
|
|
2452
|
+
import { useState as useState11 } from "react";
|
|
2453
|
+
import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
2454
|
+
function DashboardNavbar() {
|
|
2455
|
+
const pathname = usePathname();
|
|
2456
|
+
const { data: session } = useSession();
|
|
2457
|
+
const [mobileOpen, setMobileOpen] = useState11(false);
|
|
2458
|
+
const { branding, roles, strings, routes } = useGavaConfig();
|
|
2459
|
+
const role = session?.user?.role;
|
|
2460
|
+
const navigation = [
|
|
2461
|
+
{ name: strings.articles, href: routes.articles, icon: FileText3 },
|
|
2462
|
+
...roles.canEdit(role || "") ? [{ name: strings.media, href: routes.media, icon: ImageIcon3 }] : [],
|
|
2463
|
+
...roles.canPublish(role || "") ? [
|
|
2464
|
+
{
|
|
2465
|
+
name: strings.statistics,
|
|
2466
|
+
href: routes.dashboard + "/statistiche",
|
|
2467
|
+
icon: BarChart3
|
|
2468
|
+
}
|
|
2469
|
+
] : [],
|
|
2470
|
+
...role === roles.adminRole ? [{ name: strings.users, href: routes.users, icon: Users }] : []
|
|
2471
|
+
];
|
|
2472
|
+
const isActive = (href) => pathname.startsWith(href);
|
|
2473
|
+
return /* @__PURE__ */ jsx14("header", { className: "sticky top-0 z-50 px-4 pb-4", children: /* @__PURE__ */ jsx14("div", { className: "mx-auto max-w-7xl", children: /* @__PURE__ */ jsxs12("nav", { className: "bg-background/95 backdrop-blur-sm border border-t-transparent border-card-border rounded-b-xl", children: [
|
|
2474
|
+
/* @__PURE__ */ jsxs12("div", { className: "flex h-14 items-center justify-between px-6", children: [
|
|
2475
|
+
/* @__PURE__ */ jsxs12(Link4, { href: routes.home, className: "flex items-center gap-3", children: [
|
|
2476
|
+
branding.logo && /* @__PURE__ */ jsx14("span", { className: "relative z-[9999] -my-[100px] py-[100px] bg-white rounded-b-lg px-1.5 pb-1.5", children: /* @__PURE__ */ jsx14(
|
|
2477
|
+
"img",
|
|
2478
|
+
{
|
|
2479
|
+
src: branding.logo,
|
|
2480
|
+
alt: branding.name,
|
|
2481
|
+
width: 44,
|
|
2482
|
+
height: 44
|
|
2483
|
+
}
|
|
2484
|
+
) }),
|
|
2485
|
+
/* @__PURE__ */ jsx14("span", { className: "font-bold text-lg text-foreground", children: branding.name })
|
|
2486
|
+
] }),
|
|
2487
|
+
/* @__PURE__ */ jsx14("div", { className: "hidden md:flex md:items-center md:gap-6", children: navigation.map((item) => /* @__PURE__ */ jsxs12(
|
|
2488
|
+
Link4,
|
|
2489
|
+
{
|
|
2490
|
+
href: item.href,
|
|
2491
|
+
className: `flex items-center gap-2 text-sm font-medium transition-colors ${isActive(item.href) ? "text-accent" : "text-muted hover:text-foreground"}`,
|
|
2492
|
+
children: [
|
|
2493
|
+
/* @__PURE__ */ jsx14(item.icon, { className: "w-4 h-4" }),
|
|
2494
|
+
item.name
|
|
2495
|
+
]
|
|
2496
|
+
},
|
|
2497
|
+
item.name
|
|
2498
|
+
)) }),
|
|
2499
|
+
/* @__PURE__ */ jsxs12("div", { className: "hidden md:flex md:items-center md:gap-3", children: [
|
|
2500
|
+
/* @__PURE__ */ jsxs12("div", { className: "text-right", children: [
|
|
2501
|
+
/* @__PURE__ */ jsx14("p", { className: "text-sm font-medium text-foreground leading-tight", children: session?.user?.name }),
|
|
2502
|
+
/* @__PURE__ */ jsx14("p", { className: "text-xs text-muted leading-tight", children: roles.labels[role || ""] || role })
|
|
2503
|
+
] }),
|
|
2504
|
+
/* @__PURE__ */ jsx14(
|
|
2505
|
+
"button",
|
|
2506
|
+
{
|
|
2507
|
+
onClick: () => signOut({ callbackUrl: routes.login }),
|
|
2508
|
+
className: "p-2 rounded-full text-muted hover:text-foreground hover:bg-card transition-colors",
|
|
2509
|
+
title: strings.logout,
|
|
2510
|
+
children: /* @__PURE__ */ jsx14(LogOut, { className: "w-4 h-4" })
|
|
2511
|
+
}
|
|
2512
|
+
)
|
|
2513
|
+
] }),
|
|
2514
|
+
/* @__PURE__ */ jsxs12(
|
|
2515
|
+
"button",
|
|
2516
|
+
{
|
|
2517
|
+
type: "button",
|
|
2518
|
+
onClick: () => setMobileOpen(!mobileOpen),
|
|
2519
|
+
className: "md:hidden p-2 rounded-full text-foreground flex flex-col justify-center items-center w-10 h-10 gap-1.5",
|
|
2520
|
+
"aria-label": "Menu",
|
|
2521
|
+
children: [
|
|
2522
|
+
/* @__PURE__ */ jsx14(
|
|
2523
|
+
"span",
|
|
2524
|
+
{
|
|
2525
|
+
className: `block h-0.5 w-5 bg-foreground rounded-full transition-all duration-300 ease-in-out ${mobileOpen ? "rotate-45 translate-y-2" : ""}`
|
|
2526
|
+
}
|
|
2527
|
+
),
|
|
2528
|
+
/* @__PURE__ */ jsx14(
|
|
2529
|
+
"span",
|
|
2530
|
+
{
|
|
2531
|
+
className: `block h-0.5 w-5 bg-foreground rounded-full transition-all duration-300 ease-in-out ${mobileOpen ? "opacity-0 scale-0" : ""}`
|
|
2532
|
+
}
|
|
2533
|
+
),
|
|
2534
|
+
/* @__PURE__ */ jsx14(
|
|
2535
|
+
"span",
|
|
2536
|
+
{
|
|
2537
|
+
className: `block h-0.5 w-5 bg-foreground rounded-full transition-all duration-300 ease-in-out ${mobileOpen ? "-rotate-45 -translate-y-2" : ""}`
|
|
2538
|
+
}
|
|
2539
|
+
)
|
|
2540
|
+
]
|
|
2541
|
+
}
|
|
2542
|
+
)
|
|
2543
|
+
] }),
|
|
2544
|
+
/* @__PURE__ */ jsx14(
|
|
2545
|
+
"div",
|
|
2546
|
+
{
|
|
2547
|
+
className: `md:hidden overflow-hidden transition-all duration-300 ease-in-out ${mobileOpen ? "max-h-80 opacity-100" : "max-h-0 opacity-0"}`,
|
|
2548
|
+
children: /* @__PURE__ */ jsxs12("div", { className: "py-4 border-t border-card-border mx-6 space-y-1", children: [
|
|
2549
|
+
navigation.map((item) => /* @__PURE__ */ jsxs12(
|
|
2550
|
+
Link4,
|
|
2551
|
+
{
|
|
2552
|
+
href: item.href,
|
|
2553
|
+
onClick: () => setMobileOpen(false),
|
|
2554
|
+
className: `flex items-center gap-3 px-3 py-2 rounded-full text-sm font-medium transition-colors ${isActive(item.href) ? "text-accent" : "text-muted hover:text-foreground"}`,
|
|
2555
|
+
children: [
|
|
2556
|
+
/* @__PURE__ */ jsx14(item.icon, { className: "w-4 h-4" }),
|
|
2557
|
+
item.name
|
|
2558
|
+
]
|
|
2559
|
+
},
|
|
2560
|
+
item.name
|
|
2561
|
+
)),
|
|
2562
|
+
/* @__PURE__ */ jsxs12("div", { className: "pt-3 mt-2 border-t border-card-border", children: [
|
|
2563
|
+
/* @__PURE__ */ jsxs12("div", { className: "px-3 mb-2", children: [
|
|
2564
|
+
/* @__PURE__ */ jsx14("p", { className: "text-sm font-medium text-foreground", children: session?.user?.name }),
|
|
2565
|
+
/* @__PURE__ */ jsx14("p", { className: "text-xs text-muted", children: roles.labels[role || ""] || role })
|
|
2566
|
+
] }),
|
|
2567
|
+
/* @__PURE__ */ jsxs12(
|
|
2568
|
+
"button",
|
|
2569
|
+
{
|
|
2570
|
+
onClick: () => signOut({ callbackUrl: routes.login }),
|
|
2571
|
+
className: "flex items-center gap-3 px-3 py-2 text-sm text-muted hover:text-foreground transition-colors w-full rounded-full hover:bg-card",
|
|
2572
|
+
children: [
|
|
2573
|
+
/* @__PURE__ */ jsx14(LogOut, { className: "w-4 h-4" }),
|
|
2574
|
+
strings.logout
|
|
2575
|
+
]
|
|
2576
|
+
}
|
|
2577
|
+
)
|
|
2578
|
+
] })
|
|
2579
|
+
] })
|
|
2580
|
+
}
|
|
2581
|
+
)
|
|
2582
|
+
] }) }) });
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
// src/components/dashboard/StatCard.tsx
|
|
2586
|
+
import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
2587
|
+
var accentStyles = {
|
|
2588
|
+
blue: {
|
|
2589
|
+
border: "border-l-blue-500",
|
|
2590
|
+
bg: "bg-blue-500/10",
|
|
2591
|
+
text: "text-blue-600"
|
|
2592
|
+
},
|
|
2593
|
+
green: {
|
|
2594
|
+
border: "border-l-green-500",
|
|
2595
|
+
bg: "bg-green-500/10",
|
|
2596
|
+
text: "text-green-600"
|
|
2597
|
+
},
|
|
2598
|
+
purple: {
|
|
2599
|
+
border: "border-l-purple-500",
|
|
2600
|
+
bg: "bg-purple-500/10",
|
|
2601
|
+
text: "text-purple-600"
|
|
2602
|
+
},
|
|
2603
|
+
amber: {
|
|
2604
|
+
border: "border-l-amber-500",
|
|
2605
|
+
bg: "bg-amber-500/10",
|
|
2606
|
+
text: "text-amber-600"
|
|
2607
|
+
}
|
|
2608
|
+
};
|
|
2609
|
+
function StatCard({
|
|
2610
|
+
label,
|
|
2611
|
+
value,
|
|
2612
|
+
icon: Icon,
|
|
2613
|
+
accent = "blue"
|
|
2614
|
+
}) {
|
|
2615
|
+
const s = accentStyles[accent];
|
|
2616
|
+
return /* @__PURE__ */ jsx15(
|
|
2617
|
+
"div",
|
|
2618
|
+
{
|
|
2619
|
+
className: `bg-card border border-card-border rounded-xl p-5 border-l-[3px] ${s.border} transition-all duration-200 hover:shadow-sm`,
|
|
2620
|
+
children: /* @__PURE__ */ jsxs13("div", { className: "flex items-start justify-between", children: [
|
|
2621
|
+
/* @__PURE__ */ jsxs13("div", { children: [
|
|
2622
|
+
/* @__PURE__ */ jsx15("p", { className: "text-xs font-medium text-muted uppercase tracking-wide mb-2", children: label }),
|
|
2623
|
+
/* @__PURE__ */ jsx15("p", { className: "text-3xl font-bold text-foreground tabular-nums", children: value })
|
|
2624
|
+
] }),
|
|
2625
|
+
/* @__PURE__ */ jsx15("div", { className: `p-2 rounded-lg ${s.bg}`, children: /* @__PURE__ */ jsx15(Icon, { className: `w-5 h-5 ${s.text}` }) })
|
|
2626
|
+
] })
|
|
2627
|
+
}
|
|
2628
|
+
);
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
// src/components/splash/SplashScreen.tsx
|
|
2632
|
+
import { useMemo } from "react";
|
|
2633
|
+
import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
2634
|
+
function pickRandom3(words) {
|
|
2635
|
+
const shuffled = [...words].sort(() => Math.random() - 0.5);
|
|
2636
|
+
return shuffled.slice(0, 3);
|
|
2637
|
+
}
|
|
2638
|
+
function SplashScreen() {
|
|
2639
|
+
const { phase } = useSplash();
|
|
2640
|
+
const { branding } = useGavaConfig();
|
|
2641
|
+
const words = useMemo(() => pickRandom3(branding.splashWords), [branding.splashWords]);
|
|
2642
|
+
if (phase === "idle") return null;
|
|
2643
|
+
const topClass = phase === "closing" ? "splash-enter-down" : phase === "opening" ? "splash-exit-up" : "";
|
|
2644
|
+
const bottomClass = phase === "closing" ? "splash-enter-up" : phase === "opening" ? "splash-exit-down" : "";
|
|
2645
|
+
return /* @__PURE__ */ jsxs14("div", { className: "fixed inset-0 z-[9999] pointer-events-none", children: [
|
|
2646
|
+
/* @__PURE__ */ jsx16(
|
|
2647
|
+
"div",
|
|
2648
|
+
{
|
|
2649
|
+
className: `splash-half splash-half-top absolute inset-x-0 top-0 h-1/2 bg-background overflow-hidden ${topClass}`,
|
|
2650
|
+
children: /* @__PURE__ */ jsx16("div", { className: "absolute inset-0 flex flex-col items-center justify-end pb-2", children: /* @__PURE__ */ jsx16("span", { className: "splash-title", children: "GAVA" }) })
|
|
2651
|
+
}
|
|
2652
|
+
),
|
|
2653
|
+
/* @__PURE__ */ jsx16(
|
|
2654
|
+
"div",
|
|
2655
|
+
{
|
|
2656
|
+
className: `splash-half splash-half-bottom absolute inset-x-0 bottom-0 h-1/2 bg-background overflow-hidden ${bottomClass}`,
|
|
2657
|
+
children: /* @__PURE__ */ jsxs14("div", { className: "absolute inset-0 flex flex-col items-center justify-start pt-2", children: [
|
|
2658
|
+
/* @__PURE__ */ jsx16("span", { className: "splash-title", children: "ENGINE" }),
|
|
2659
|
+
/* @__PURE__ */ jsx16("div", { className: "flex items-center gap-3 mt-6", children: words.map((word, i) => /* @__PURE__ */ jsxs14(
|
|
2660
|
+
"span",
|
|
2661
|
+
{
|
|
2662
|
+
className: "splash-word text-xs sm:text-sm font-medium tracking-widest uppercase",
|
|
2663
|
+
style: { animationDelay: `${0.2 + i * 0.1}s` },
|
|
2664
|
+
children: [
|
|
2665
|
+
word,
|
|
2666
|
+
i < 2 && /* @__PURE__ */ jsx16("span", { className: "ml-3 text-muted-foreground", children: "\xB7" })
|
|
2667
|
+
]
|
|
2668
|
+
},
|
|
2669
|
+
word
|
|
2670
|
+
)) })
|
|
2671
|
+
] })
|
|
2672
|
+
}
|
|
2673
|
+
)
|
|
2674
|
+
] });
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
// src/components/splash/DashboardSplashTrigger.tsx
|
|
2678
|
+
import { useEffect as useEffect6 } from "react";
|
|
2679
|
+
function DashboardSplashTrigger() {
|
|
2680
|
+
const { openSplash } = useSplash();
|
|
2681
|
+
useEffect6(() => {
|
|
2682
|
+
if (typeof window.requestIdleCallback === "function") {
|
|
2683
|
+
const id = window.requestIdleCallback(() => openSplash());
|
|
2684
|
+
return () => window.cancelIdleCallback(id);
|
|
2685
|
+
}
|
|
2686
|
+
const timeout = setTimeout(() => {
|
|
2687
|
+
requestAnimationFrame(() => openSplash());
|
|
2688
|
+
}, 100);
|
|
2689
|
+
return () => clearTimeout(timeout);
|
|
2690
|
+
}, [openSplash]);
|
|
2691
|
+
return null;
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
export {
|
|
2695
|
+
ResizableImageView,
|
|
2696
|
+
ResizableImage,
|
|
2697
|
+
DraggableYoutubeView,
|
|
2698
|
+
DraggableYoutube,
|
|
2699
|
+
VideoView,
|
|
2700
|
+
VideoExtension,
|
|
2701
|
+
MediaPickerModal,
|
|
2702
|
+
EditorToolbar,
|
|
2703
|
+
CoverImageUpload,
|
|
2704
|
+
RevisionPanel,
|
|
2705
|
+
ArticleEditor,
|
|
2706
|
+
ImageEditModal,
|
|
2707
|
+
ArticleList,
|
|
2708
|
+
UserTable,
|
|
2709
|
+
UserForm,
|
|
2710
|
+
MediaGrid,
|
|
2711
|
+
DashboardNavbar,
|
|
2712
|
+
StatCard,
|
|
2713
|
+
SplashScreen,
|
|
2714
|
+
DashboardSplashTrigger
|
|
2715
|
+
};
|
|
2716
|
+
//# sourceMappingURL=chunk-CE7E5MAB.js.map
|