@ampless/admin 0.2.0-alpha.2 → 0.2.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +73 -0
- package/README.md +3 -0
- package/dist/api/index.d.ts +1 -1
- package/dist/chunk-2ITWLRYF.js +38 -0
- package/dist/chunk-5JKOPRCO.js +41 -0
- package/dist/chunk-5Q6KVRZ2.js +250 -0
- package/dist/chunk-7IR4F7GA.js +6 -0
- package/dist/chunk-A3SWBQA6.js +71 -0
- package/dist/chunk-BC4B6DLO.js +21 -0
- package/dist/chunk-BWFCQNPU.js +1264 -0
- package/dist/chunk-CQY55RDG.js +48 -0
- package/dist/chunk-CVJCMTYB.js +1197 -0
- package/dist/chunk-JOASK4AM.js +360 -0
- package/dist/{chunk-TJR3ALRJ.js → chunk-OSUTPPAU.js} +171 -68
- package/dist/chunk-QXJIIBUQ.js +21 -0
- package/dist/chunk-S66L5CDS.js +335 -0
- package/dist/chunk-SRNH2IVA.js +149 -0
- package/dist/chunk-TZWSXAHD.js +32 -0
- package/dist/chunk-VXEVLHGL.js +10 -0
- package/dist/chunk-W6BXESPW.js +198 -0
- package/dist/chunk-XY4JWSMS.js +33 -0
- package/dist/components/admin-dashboard.d.ts +10 -0
- package/dist/components/admin-dashboard.js +9 -0
- package/dist/components/edit-post-view.d.ts +9 -0
- package/dist/components/edit-post-view.js +14 -0
- package/dist/components/index.d.ts +40 -21
- package/dist/components/index.js +32 -15
- package/dist/components/login-view.d.ts +5 -0
- package/dist/components/login-view.js +9 -0
- package/dist/components/mcp-tokens-view.d.ts +22 -0
- package/dist/components/mcp-tokens-view.js +9 -0
- package/dist/components/media-view.d.ts +5 -0
- package/dist/components/media-view.js +12 -0
- package/dist/components/new-post-view.d.ts +5 -0
- package/dist/components/new-post-view.js +14 -0
- package/dist/components/posts-list-view.d.ts +5 -0
- package/dist/components/posts-list-view.js +11 -0
- package/dist/components/users-list-view.d.ts +7 -0
- package/dist/components/users-list-view.js +9 -0
- package/dist/{i18n-ByHM_Bho.d.ts → i18n-MWvAMHzn.d.ts} +253 -2
- package/dist/index.d.ts +14 -9
- package/dist/index.js +18 -10
- package/dist/lib/theme-actions.d.ts +17 -0
- package/dist/lib/theme-actions.js +7 -0
- package/dist/metafile-esm.json +1 -1
- package/dist/pages/index.d.ts +67 -15
- package/dist/pages/index.js +147 -659
- package/package.json +12 -9
- package/dist/chunk-T2RSMFOI.js +0 -2074
|
@@ -0,0 +1,1197 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import {
|
|
3
|
+
readAdminSiteIdFromCookie
|
|
4
|
+
} from "./chunk-TZWSXAHD.js";
|
|
5
|
+
import {
|
|
6
|
+
ImageUploadDialog,
|
|
7
|
+
getMediaProcessingDefaults
|
|
8
|
+
} from "./chunk-S66L5CDS.js";
|
|
9
|
+
import {
|
|
10
|
+
publicMediaUrl
|
|
11
|
+
} from "./chunk-2ITWLRYF.js";
|
|
12
|
+
import {
|
|
13
|
+
useT
|
|
14
|
+
} from "./chunk-XY4JWSMS.js";
|
|
15
|
+
|
|
16
|
+
// src/lib/upload.ts
|
|
17
|
+
import { uploadData } from "aws-amplify/storage";
|
|
18
|
+
import { processImage } from "ampless/media";
|
|
19
|
+
function sanitizeName(name) {
|
|
20
|
+
return name.replace(/[\x00-\x1f\x7f]/g, "").replace(/[\\/:*?"<>|]/g, "_").replace(/\s+/g, "_").replace(/^\.+/, "_").slice(0, 200) || "upload";
|
|
21
|
+
}
|
|
22
|
+
async function uploadProcessedImage(file, options) {
|
|
23
|
+
const processed = await processImage(file, options);
|
|
24
|
+
const safeName = sanitizeName(processed.suggestedName);
|
|
25
|
+
const now = /* @__PURE__ */ new Date();
|
|
26
|
+
const yyyy = now.getFullYear();
|
|
27
|
+
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
|
28
|
+
const path = `public/media/${yyyy}/${mm}/${Date.now()}-${safeName}`;
|
|
29
|
+
await uploadData({
|
|
30
|
+
path,
|
|
31
|
+
data: processed.blob,
|
|
32
|
+
options: { contentType: processed.mime }
|
|
33
|
+
}).result;
|
|
34
|
+
return { path, url: publicMediaUrl(path) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/components/media-picker.tsx
|
|
38
|
+
import { useEffect, useRef, useState } from "react";
|
|
39
|
+
import { list } from "aws-amplify/storage";
|
|
40
|
+
import { Upload } from "lucide-react";
|
|
41
|
+
import {
|
|
42
|
+
Dialog,
|
|
43
|
+
DialogContent,
|
|
44
|
+
DialogDescription,
|
|
45
|
+
DialogHeader,
|
|
46
|
+
DialogTitle,
|
|
47
|
+
DialogTrigger,
|
|
48
|
+
Button
|
|
49
|
+
} from "@ampless/runtime/ui";
|
|
50
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
51
|
+
function MediaPicker({ trigger, onSelect }) {
|
|
52
|
+
const t = useT();
|
|
53
|
+
const [open, setOpen] = useState(false);
|
|
54
|
+
const [items, setItems] = useState([]);
|
|
55
|
+
const [loading, setLoading] = useState(false);
|
|
56
|
+
const [error, setError] = useState(null);
|
|
57
|
+
const [pendingUpload, setPendingUpload] = useState(null);
|
|
58
|
+
const [uploading, setUploading] = useState(false);
|
|
59
|
+
const fileInputRef = useRef(null);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!open) return;
|
|
62
|
+
let cancelled = false;
|
|
63
|
+
setLoading(true);
|
|
64
|
+
setError(null);
|
|
65
|
+
list({ path: "public/media/" }).then((result) => {
|
|
66
|
+
if (cancelled) return;
|
|
67
|
+
setItems(result.items.map((i) => i.path));
|
|
68
|
+
}).catch((err) => {
|
|
69
|
+
if (!cancelled) setError(err instanceof Error ? err.message : String(err));
|
|
70
|
+
}).finally(() => {
|
|
71
|
+
if (!cancelled) setLoading(false);
|
|
72
|
+
});
|
|
73
|
+
return () => {
|
|
74
|
+
cancelled = true;
|
|
75
|
+
};
|
|
76
|
+
}, [open]);
|
|
77
|
+
function handlePick(path) {
|
|
78
|
+
onSelect(publicMediaUrl(path));
|
|
79
|
+
setOpen(false);
|
|
80
|
+
}
|
|
81
|
+
function handleFileSelected(e) {
|
|
82
|
+
const file = e.target.files?.[0];
|
|
83
|
+
e.target.value = "";
|
|
84
|
+
if (!file) return;
|
|
85
|
+
setPendingUpload(file);
|
|
86
|
+
}
|
|
87
|
+
async function handleUploadConfirm(file, options) {
|
|
88
|
+
setUploading(true);
|
|
89
|
+
setError(null);
|
|
90
|
+
try {
|
|
91
|
+
const { url } = await uploadProcessedImage(file, options);
|
|
92
|
+
onSelect(url);
|
|
93
|
+
setPendingUpload(null);
|
|
94
|
+
setOpen(false);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
97
|
+
} finally {
|
|
98
|
+
setUploading(false);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const pickerOpen = open && !pendingUpload;
|
|
102
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
103
|
+
/* @__PURE__ */ jsxs(Dialog, { open: pickerOpen, onOpenChange: (next) => setOpen(next), children: [
|
|
104
|
+
/* @__PURE__ */ jsx(DialogTrigger, { asChild: true, onClick: () => setOpen(true), children: trigger }),
|
|
105
|
+
/* @__PURE__ */ jsxs(DialogContent, { children: [
|
|
106
|
+
/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-4", children: [
|
|
107
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
108
|
+
/* @__PURE__ */ jsx(DialogTitle, { children: t("mediaPicker.title") }),
|
|
109
|
+
/* @__PURE__ */ jsx(DialogDescription, { children: t("mediaPicker.description") })
|
|
110
|
+
] }),
|
|
111
|
+
/* @__PURE__ */ jsxs(
|
|
112
|
+
Button,
|
|
113
|
+
{
|
|
114
|
+
type: "button",
|
|
115
|
+
variant: "outline",
|
|
116
|
+
size: "sm",
|
|
117
|
+
onClick: () => fileInputRef.current?.click(),
|
|
118
|
+
children: [
|
|
119
|
+
/* @__PURE__ */ jsx(Upload, { className: "mr-2 h-3 w-3" }),
|
|
120
|
+
t("mediaPicker.uploadNew")
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
] }) }),
|
|
125
|
+
/* @__PURE__ */ jsx(
|
|
126
|
+
"input",
|
|
127
|
+
{
|
|
128
|
+
ref: fileInputRef,
|
|
129
|
+
type: "file",
|
|
130
|
+
accept: "image/*",
|
|
131
|
+
className: "hidden",
|
|
132
|
+
onChange: handleFileSelected
|
|
133
|
+
}
|
|
134
|
+
),
|
|
135
|
+
loading && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("common.loading") }),
|
|
136
|
+
error && /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: error }),
|
|
137
|
+
!loading && items.length === 0 && /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
|
|
138
|
+
t("mediaPicker.empty"),
|
|
139
|
+
" ",
|
|
140
|
+
t("mediaPicker.emptyHint", { action: t("mediaPicker.emptyAction") })
|
|
141
|
+
] }),
|
|
142
|
+
/* @__PURE__ */ jsx("div", { className: "grid max-h-[60vh] grid-cols-3 gap-3 overflow-auto sm:grid-cols-4", children: items.map((path) => /* @__PURE__ */ jsxs(
|
|
143
|
+
"button",
|
|
144
|
+
{
|
|
145
|
+
type: "button",
|
|
146
|
+
onClick: () => handlePick(path),
|
|
147
|
+
className: "group overflow-hidden rounded-md border transition hover:border-primary",
|
|
148
|
+
children: [
|
|
149
|
+
/* @__PURE__ */ jsx(
|
|
150
|
+
"img",
|
|
151
|
+
{
|
|
152
|
+
src: publicMediaUrl(path),
|
|
153
|
+
alt: path,
|
|
154
|
+
className: "aspect-square w-full object-cover"
|
|
155
|
+
}
|
|
156
|
+
),
|
|
157
|
+
/* @__PURE__ */ jsx("div", { className: "truncate p-1 text-xs text-muted-foreground", children: path.split("/").pop() })
|
|
158
|
+
]
|
|
159
|
+
},
|
|
160
|
+
path
|
|
161
|
+
)) })
|
|
162
|
+
] })
|
|
163
|
+
] }),
|
|
164
|
+
/* @__PURE__ */ jsx(
|
|
165
|
+
ImageUploadDialog,
|
|
166
|
+
{
|
|
167
|
+
file: pendingUpload,
|
|
168
|
+
remaining: pendingUpload ? 1 : 0,
|
|
169
|
+
busy: uploading,
|
|
170
|
+
defaults: getMediaProcessingDefaults(),
|
|
171
|
+
onConfirm: handleUploadConfirm,
|
|
172
|
+
onSkip: () => setPendingUpload(null),
|
|
173
|
+
onCancel: () => setPendingUpload(null)
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
] });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/components/post-form.tsx
|
|
180
|
+
import { useRef as useRef2, useState as useState3 } from "react";
|
|
181
|
+
import { useRouter } from "next/navigation";
|
|
182
|
+
import { Image as ImageIcon3 } from "lucide-react";
|
|
183
|
+
import {
|
|
184
|
+
createPost,
|
|
185
|
+
updatePost,
|
|
186
|
+
deletePost,
|
|
187
|
+
formatDate
|
|
188
|
+
} from "ampless";
|
|
189
|
+
import {
|
|
190
|
+
renderBody,
|
|
191
|
+
tiptapToHtml,
|
|
192
|
+
tiptapToMarkdown,
|
|
193
|
+
markdownToHtml,
|
|
194
|
+
htmlToMarkdown
|
|
195
|
+
} from "@ampless/runtime";
|
|
196
|
+
import { Button as Button5, Input, Label, Textarea } from "@ampless/runtime/ui";
|
|
197
|
+
|
|
198
|
+
// src/editor/tiptap-editor.tsx
|
|
199
|
+
import { useEditor, EditorContent } from "@tiptap/react";
|
|
200
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
201
|
+
import Link from "@tiptap/extension-link";
|
|
202
|
+
import Image from "@tiptap/extension-image";
|
|
203
|
+
|
|
204
|
+
// src/editor/toolbar.tsx
|
|
205
|
+
import {
|
|
206
|
+
Bold,
|
|
207
|
+
Italic,
|
|
208
|
+
Heading1,
|
|
209
|
+
Heading2,
|
|
210
|
+
List,
|
|
211
|
+
ListOrdered,
|
|
212
|
+
Code,
|
|
213
|
+
Link as LinkIcon,
|
|
214
|
+
Image as ImageIcon
|
|
215
|
+
} from "lucide-react";
|
|
216
|
+
import { Button as Button2, cn } from "@ampless/runtime/ui";
|
|
217
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
218
|
+
function Toolbar({ editor }) {
|
|
219
|
+
const t = useT();
|
|
220
|
+
if (!editor) return null;
|
|
221
|
+
const tools = [
|
|
222
|
+
{ name: "bold", icon: Bold, action: () => editor.chain().focus().toggleBold().run(), isActive: () => editor.isActive("bold") },
|
|
223
|
+
{ name: "italic", icon: Italic, action: () => editor.chain().focus().toggleItalic().run(), isActive: () => editor.isActive("italic") },
|
|
224
|
+
{ name: "h1", icon: Heading1, action: () => editor.chain().focus().toggleHeading({ level: 1 }).run(), isActive: () => editor.isActive("heading", { level: 1 }) },
|
|
225
|
+
{ name: "h2", icon: Heading2, action: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), isActive: () => editor.isActive("heading", { level: 2 }) },
|
|
226
|
+
{ name: "bulletList", icon: List, action: () => editor.chain().focus().toggleBulletList().run(), isActive: () => editor.isActive("bulletList") },
|
|
227
|
+
{ name: "orderedList", icon: ListOrdered, action: () => editor.chain().focus().toggleOrderedList().run(), isActive: () => editor.isActive("orderedList") },
|
|
228
|
+
{ name: "code", icon: Code, action: () => editor.chain().focus().toggleCodeBlock().run(), isActive: () => editor.isActive("codeBlock") }
|
|
229
|
+
];
|
|
230
|
+
const setLink = () => {
|
|
231
|
+
const previousUrl = editor.getAttributes("link").href ?? "";
|
|
232
|
+
const url = window.prompt(t("editor.linkPrompt"), previousUrl);
|
|
233
|
+
if (url === null) return;
|
|
234
|
+
if (url === "") {
|
|
235
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
|
|
239
|
+
};
|
|
240
|
+
const insertImage = (url) => {
|
|
241
|
+
editor.chain().focus().setImage({ src: url }).run();
|
|
242
|
+
};
|
|
243
|
+
return /* @__PURE__ */ jsxs2("div", { className: "flex flex-wrap gap-1 border-b p-2", children: [
|
|
244
|
+
tools.map((tool) => {
|
|
245
|
+
const Icon = tool.icon;
|
|
246
|
+
return /* @__PURE__ */ jsx2(
|
|
247
|
+
Button2,
|
|
248
|
+
{
|
|
249
|
+
type: "button",
|
|
250
|
+
variant: "ghost",
|
|
251
|
+
size: "icon",
|
|
252
|
+
onClick: tool.action,
|
|
253
|
+
className: cn(tool.isActive() && "bg-accent text-accent-foreground"),
|
|
254
|
+
children: /* @__PURE__ */ jsx2(Icon, { className: "h-4 w-4" })
|
|
255
|
+
},
|
|
256
|
+
tool.name
|
|
257
|
+
);
|
|
258
|
+
}),
|
|
259
|
+
/* @__PURE__ */ jsx2(
|
|
260
|
+
Button2,
|
|
261
|
+
{
|
|
262
|
+
type: "button",
|
|
263
|
+
variant: "ghost",
|
|
264
|
+
size: "icon",
|
|
265
|
+
onClick: setLink,
|
|
266
|
+
className: cn(editor.isActive("link") && "bg-accent text-accent-foreground"),
|
|
267
|
+
children: /* @__PURE__ */ jsx2(LinkIcon, { className: "h-4 w-4" })
|
|
268
|
+
}
|
|
269
|
+
),
|
|
270
|
+
/* @__PURE__ */ jsx2(
|
|
271
|
+
MediaPicker,
|
|
272
|
+
{
|
|
273
|
+
onSelect: insertImage,
|
|
274
|
+
trigger: /* @__PURE__ */ jsx2(Button2, { type: "button", variant: "ghost", size: "icon", children: /* @__PURE__ */ jsx2(ImageIcon, { className: "h-4 w-4" }) })
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
] });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/editor/image-bubble-menu.tsx
|
|
281
|
+
import { BubbleMenu } from "@tiptap/react/menus";
|
|
282
|
+
import { Trash2, Pencil, ImageIcon as ImageIcon2, Maximize2 } from "lucide-react";
|
|
283
|
+
import { Button as Button3, cn as cn2 } from "@ampless/runtime/ui";
|
|
284
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
285
|
+
function ImageBubbleMenu({ editor }) {
|
|
286
|
+
const t = useT();
|
|
287
|
+
const editAlt = () => {
|
|
288
|
+
const current = editor.getAttributes("image").alt ?? "";
|
|
289
|
+
const alt = window.prompt(t("editor.altPrompt"), current);
|
|
290
|
+
if (alt === null) return;
|
|
291
|
+
editor.chain().focus().updateAttributes("image", { alt }).run();
|
|
292
|
+
};
|
|
293
|
+
const remove2 = () => {
|
|
294
|
+
editor.chain().focus().deleteSelection().run();
|
|
295
|
+
};
|
|
296
|
+
const setDisplay = (display) => {
|
|
297
|
+
const current = editor.getAttributes("image").display ?? null;
|
|
298
|
+
const next = current === display ? null : display;
|
|
299
|
+
editor.chain().focus().updateAttributes("image", { display: next }).run();
|
|
300
|
+
};
|
|
301
|
+
const currentDisplay = editor.getAttributes("image").display ?? null;
|
|
302
|
+
return /* @__PURE__ */ jsx3(
|
|
303
|
+
BubbleMenu,
|
|
304
|
+
{
|
|
305
|
+
editor,
|
|
306
|
+
shouldShow: ({ editor: editor2 }) => editor2.isActive("image"),
|
|
307
|
+
options: { placement: "top" },
|
|
308
|
+
children: /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1 rounded-md border bg-popover p-1 shadow", children: [
|
|
309
|
+
/* @__PURE__ */ jsxs3(
|
|
310
|
+
Button3,
|
|
311
|
+
{
|
|
312
|
+
type: "button",
|
|
313
|
+
variant: "ghost",
|
|
314
|
+
size: "sm",
|
|
315
|
+
onClick: () => setDisplay("inline"),
|
|
316
|
+
className: cn2(currentDisplay === "inline" && "bg-accent text-accent-foreground"),
|
|
317
|
+
title: t("editor.image.inlineTitle"),
|
|
318
|
+
children: [
|
|
319
|
+
/* @__PURE__ */ jsx3(ImageIcon2, { className: "mr-1 h-3 w-3" }),
|
|
320
|
+
t("editor.image.inline")
|
|
321
|
+
]
|
|
322
|
+
}
|
|
323
|
+
),
|
|
324
|
+
/* @__PURE__ */ jsxs3(
|
|
325
|
+
Button3,
|
|
326
|
+
{
|
|
327
|
+
type: "button",
|
|
328
|
+
variant: "ghost",
|
|
329
|
+
size: "sm",
|
|
330
|
+
onClick: () => setDisplay("lightbox"),
|
|
331
|
+
className: cn2(currentDisplay === "lightbox" && "bg-accent text-accent-foreground"),
|
|
332
|
+
title: t("editor.image.lightboxTitle"),
|
|
333
|
+
children: [
|
|
334
|
+
/* @__PURE__ */ jsx3(Maximize2, { className: "mr-1 h-3 w-3" }),
|
|
335
|
+
t("editor.image.lightbox")
|
|
336
|
+
]
|
|
337
|
+
}
|
|
338
|
+
),
|
|
339
|
+
/* @__PURE__ */ jsx3("span", { className: "mx-1 h-4 w-px bg-border" }),
|
|
340
|
+
/* @__PURE__ */ jsxs3(Button3, { type: "button", variant: "ghost", size: "sm", onClick: editAlt, children: [
|
|
341
|
+
/* @__PURE__ */ jsx3(Pencil, { className: "mr-1 h-3 w-3" }),
|
|
342
|
+
t("editor.image.alt")
|
|
343
|
+
] }),
|
|
344
|
+
/* @__PURE__ */ jsxs3(Button3, { type: "button", variant: "ghost", size: "sm", onClick: remove2, children: [
|
|
345
|
+
/* @__PURE__ */ jsx3(Trash2, { className: "mr-1 h-3 w-3" }),
|
|
346
|
+
t("editor.image.delete")
|
|
347
|
+
] })
|
|
348
|
+
] })
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/editor/tiptap-editor.tsx
|
|
354
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
355
|
+
var AmplessImage = Image.extend({
|
|
356
|
+
addAttributes() {
|
|
357
|
+
return {
|
|
358
|
+
...this.parent?.(),
|
|
359
|
+
display: {
|
|
360
|
+
default: null,
|
|
361
|
+
parseHTML: (el) => el.getAttribute("data-display"),
|
|
362
|
+
renderHTML: (attrs) => {
|
|
363
|
+
const v = attrs.display;
|
|
364
|
+
return v ? { "data-display": v } : {};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
function TiptapEditor({ initialContent, onChange }) {
|
|
371
|
+
const editor = useEditor({
|
|
372
|
+
extensions: [
|
|
373
|
+
StarterKit,
|
|
374
|
+
Link.configure({ openOnClick: false }),
|
|
375
|
+
AmplessImage.configure({ inline: false, allowBase64: false })
|
|
376
|
+
],
|
|
377
|
+
content: initialContent ?? { type: "doc", content: [{ type: "paragraph" }] },
|
|
378
|
+
immediatelyRender: false,
|
|
379
|
+
editorProps: {
|
|
380
|
+
attributes: {
|
|
381
|
+
class: "prose prose-neutral dark:prose-invert max-w-none min-h-[400px] px-4 py-3 focus:outline-none"
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
onCreate: ({ editor: editor2 }) => {
|
|
385
|
+
onChange?.(editor2.getJSON());
|
|
386
|
+
},
|
|
387
|
+
onUpdate: ({ editor: editor2 }) => {
|
|
388
|
+
onChange?.(editor2.getJSON());
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
return /* @__PURE__ */ jsxs4("div", { className: "rounded-md border", children: [
|
|
392
|
+
/* @__PURE__ */ jsx4(Toolbar, { editor }),
|
|
393
|
+
editor && /* @__PURE__ */ jsx4(ImageBubbleMenu, { editor }),
|
|
394
|
+
/* @__PURE__ */ jsx4(EditorContent, { editor })
|
|
395
|
+
] });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/components/static-uploader.tsx
|
|
399
|
+
import { useState as useState2 } from "react";
|
|
400
|
+
import { FileText, AlertTriangle, FileArchive, X } from "lucide-react";
|
|
401
|
+
import { Button as Button4 } from "@ampless/runtime/ui";
|
|
402
|
+
|
|
403
|
+
// src/lib/static-bundle.ts
|
|
404
|
+
import JSZip from "jszip";
|
|
405
|
+
import { uploadData as uploadData2, list as list2, remove } from "aws-amplify/storage";
|
|
406
|
+
var DEFAULT_ENTRYPOINT = "index.html";
|
|
407
|
+
var MAX_BUNDLE_BYTES = 50 * 1024 * 1024;
|
|
408
|
+
var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([".html", ".htm", ".css", ".svg"]);
|
|
409
|
+
var MIME_TYPES = {
|
|
410
|
+
".html": "text/html; charset=utf-8",
|
|
411
|
+
".htm": "text/html; charset=utf-8",
|
|
412
|
+
".css": "text/css; charset=utf-8",
|
|
413
|
+
".js": "application/javascript; charset=utf-8",
|
|
414
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
415
|
+
".json": "application/json; charset=utf-8",
|
|
416
|
+
".svg": "image/svg+xml",
|
|
417
|
+
".png": "image/png",
|
|
418
|
+
".jpg": "image/jpeg",
|
|
419
|
+
".jpeg": "image/jpeg",
|
|
420
|
+
".gif": "image/gif",
|
|
421
|
+
".webp": "image/webp",
|
|
422
|
+
".avif": "image/avif",
|
|
423
|
+
".ico": "image/x-icon",
|
|
424
|
+
".woff": "font/woff",
|
|
425
|
+
".woff2": "font/woff2",
|
|
426
|
+
".ttf": "font/ttf",
|
|
427
|
+
".otf": "font/otf",
|
|
428
|
+
".eot": "application/vnd.ms-fontobject",
|
|
429
|
+
".txt": "text/plain; charset=utf-8",
|
|
430
|
+
".xml": "application/xml; charset=utf-8",
|
|
431
|
+
".map": "application/json; charset=utf-8",
|
|
432
|
+
".pdf": "application/pdf"
|
|
433
|
+
};
|
|
434
|
+
function mimeTypeFor(path) {
|
|
435
|
+
const lower = path.toLowerCase();
|
|
436
|
+
const dot = lower.lastIndexOf(".");
|
|
437
|
+
if (dot < 0) return "application/octet-stream";
|
|
438
|
+
return MIME_TYPES[lower.slice(dot)] ?? "application/octet-stream";
|
|
439
|
+
}
|
|
440
|
+
function validateBundlePath(path) {
|
|
441
|
+
if (path === "" || path.endsWith("/")) return "directory entry";
|
|
442
|
+
if (path.includes("\0")) return "contains null byte";
|
|
443
|
+
if (path.startsWith("/") || path.startsWith("\\")) return "absolute path";
|
|
444
|
+
if (path.split(/[/\\]/).some((seg) => seg === "..")) return "parent-directory traversal";
|
|
445
|
+
if (path.startsWith("__MACOSX/") || /(^|\/)\._/.test(path)) return "macOS resource fork";
|
|
446
|
+
if (/(^|\/)\.DS_Store$/.test(path)) return ".DS_Store junk";
|
|
447
|
+
if (/(^|\/)Thumbs\.db$/i.test(path)) return "Thumbs.db junk";
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
var HTML_URL_ATTR_RE = /\b(?:href|src|action|data|poster|cite|formaction|manifest|srcset)\s*=\s*["']([^"']+)["']/gi;
|
|
451
|
+
var CSS_URL_RE = /url\(\s*["']?([^"')\s]+)["']?\s*\)|@import\s+["']([^"']+)["']/gi;
|
|
452
|
+
function findAbsolutePathRefs(path, content) {
|
|
453
|
+
const ext = path.toLowerCase().slice(path.lastIndexOf("."));
|
|
454
|
+
if (!TEXT_EXTENSIONS.has(ext)) return [];
|
|
455
|
+
const issues = [];
|
|
456
|
+
function check(url, lineHint) {
|
|
457
|
+
const trimmed = url.trim();
|
|
458
|
+
if (!trimmed) return;
|
|
459
|
+
if (trimmed.startsWith("#")) return;
|
|
460
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) return;
|
|
461
|
+
if (trimmed.startsWith("//")) {
|
|
462
|
+
issues.push({ path, reason: `protocol-relative URL: ${lineHint}` });
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (trimmed.startsWith("/")) {
|
|
466
|
+
issues.push({ path, reason: `absolute path: ${lineHint}` });
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (ext === ".html" || ext === ".htm" || ext === ".svg") {
|
|
471
|
+
HTML_URL_ATTR_RE.lastIndex = 0;
|
|
472
|
+
let m;
|
|
473
|
+
while ((m = HTML_URL_ATTR_RE.exec(content)) !== null) {
|
|
474
|
+
const val = m[1] ?? "";
|
|
475
|
+
if (/\bsrcset\s*=/i.test(m[0])) {
|
|
476
|
+
for (const candidate of val.split(",")) {
|
|
477
|
+
const urlPart = candidate.trim().split(/\s+/)[0];
|
|
478
|
+
if (urlPart) check(urlPart, candidate.trim());
|
|
479
|
+
}
|
|
480
|
+
} else {
|
|
481
|
+
check(val, m[0]);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (ext === ".css" || ext === ".svg") {
|
|
486
|
+
CSS_URL_RE.lastIndex = 0;
|
|
487
|
+
let m;
|
|
488
|
+
while ((m = CSS_URL_RE.exec(content)) !== null) {
|
|
489
|
+
const val = (m[1] ?? m[2] ?? "").trim();
|
|
490
|
+
check(val, m[0]);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return issues;
|
|
494
|
+
}
|
|
495
|
+
async function extractZip(file) {
|
|
496
|
+
const zip = await JSZip.loadAsync(file);
|
|
497
|
+
const files = [];
|
|
498
|
+
const issues = [];
|
|
499
|
+
let totalBytes = 0;
|
|
500
|
+
for (const entry of Object.values(zip.files)) {
|
|
501
|
+
if (entry.dir) continue;
|
|
502
|
+
const reason = validateBundlePath(entry.name);
|
|
503
|
+
if (reason) {
|
|
504
|
+
const silent = reason === "macOS resource fork" || reason === ".DS_Store junk" || reason === "Thumbs.db junk";
|
|
505
|
+
if (!silent) issues.push({ path: entry.name, reason });
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
const data = await entry.async("uint8array");
|
|
509
|
+
totalBytes += data.byteLength;
|
|
510
|
+
files.push({ path: entry.name, data });
|
|
511
|
+
}
|
|
512
|
+
return { files: stripCommonPrefix(files), issues, totalBytes };
|
|
513
|
+
}
|
|
514
|
+
function stripCommonPrefix(files) {
|
|
515
|
+
if (files.length === 0) return files;
|
|
516
|
+
const firstSlash = files[0].path.indexOf("/");
|
|
517
|
+
if (firstSlash < 0) return files;
|
|
518
|
+
const prefix = files[0].path.slice(0, firstSlash + 1);
|
|
519
|
+
if (!files.every((f) => f.path.startsWith(prefix))) return files;
|
|
520
|
+
return files.map((f) => ({ ...f, path: f.path.slice(prefix.length) }));
|
|
521
|
+
}
|
|
522
|
+
function validateBundle(files) {
|
|
523
|
+
const issues = [];
|
|
524
|
+
const decoder = new TextDecoder("utf-8", { fatal: false });
|
|
525
|
+
for (const f of files) {
|
|
526
|
+
const ext = f.path.toLowerCase().slice(f.path.lastIndexOf("."));
|
|
527
|
+
if (!TEXT_EXTENSIONS.has(ext)) continue;
|
|
528
|
+
const text = decoder.decode(f.data);
|
|
529
|
+
issues.push(...findAbsolutePathRefs(f.path, text));
|
|
530
|
+
}
|
|
531
|
+
return issues;
|
|
532
|
+
}
|
|
533
|
+
function bundlePrefix(siteId, slug) {
|
|
534
|
+
return `public/static/${siteId}/${slug}/`;
|
|
535
|
+
}
|
|
536
|
+
async function uploadBundle(opts) {
|
|
537
|
+
if (opts.files.length === 0) {
|
|
538
|
+
throw new Error("Bundle is empty.");
|
|
539
|
+
}
|
|
540
|
+
const totalBytes = opts.files.reduce((sum, f) => sum + f.data.byteLength, 0);
|
|
541
|
+
if (totalBytes > MAX_BUNDLE_BYTES) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
`Bundle too large: ${Math.round(totalBytes / 1024 / 1024)} MB exceeds the ${Math.round(MAX_BUNDLE_BYTES / 1024 / 1024)} MB ceiling for browser-side upload.`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
const entrypoint = opts.entrypoint ?? pickDefaultEntrypoint(opts.files);
|
|
547
|
+
if (!opts.files.some((f) => f.path === entrypoint)) {
|
|
548
|
+
throw new Error(`Entrypoint "${entrypoint}" is not present in the bundle.`);
|
|
549
|
+
}
|
|
550
|
+
await deleteBundle(opts.siteId, opts.slug).catch(() => void 0);
|
|
551
|
+
const prefix = bundlePrefix(opts.siteId, opts.slug);
|
|
552
|
+
let uploaded = 0;
|
|
553
|
+
for (const f of opts.files) {
|
|
554
|
+
const task = uploadData2({
|
|
555
|
+
path: `${prefix}${f.path}`,
|
|
556
|
+
data: f.data,
|
|
557
|
+
// Forcing Content-Type at upload means CloudFront / browsers see
|
|
558
|
+
// it directly when serving the file via the public bucket URL.
|
|
559
|
+
// (The runtime route handler overrides it for the proxied path,
|
|
560
|
+
// but tooling that hits S3 directly benefits from a correct CT.)
|
|
561
|
+
options: { contentType: mimeTypeFor(f.path) }
|
|
562
|
+
});
|
|
563
|
+
await task.result;
|
|
564
|
+
uploaded += f.data.byteLength;
|
|
565
|
+
opts.onProgress?.(uploaded, totalBytes);
|
|
566
|
+
}
|
|
567
|
+
return {
|
|
568
|
+
entrypoint,
|
|
569
|
+
files: opts.files.map((f) => f.path).sort(),
|
|
570
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
async function deleteBundle(siteId, slug) {
|
|
574
|
+
const prefix = bundlePrefix(siteId, slug);
|
|
575
|
+
const result = await list2({ path: prefix });
|
|
576
|
+
for (const item of result.items) {
|
|
577
|
+
await remove({ path: item.path });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function pickDefaultEntrypoint(files) {
|
|
581
|
+
const exact = files.find((f) => f.path === DEFAULT_ENTRYPOINT);
|
|
582
|
+
if (exact) return exact.path;
|
|
583
|
+
const altRoot = files.find((f) => f.path === "index.htm");
|
|
584
|
+
if (altRoot) return altRoot.path;
|
|
585
|
+
const htmlRoot = files.filter((f) => /^[^/]+\.html?$/.test(f.path)).sort((a, b) => a.path.localeCompare(b.path));
|
|
586
|
+
if (htmlRoot.length > 0) return htmlRoot[0].path;
|
|
587
|
+
const htmlAny = files.filter((f) => /\.html?$/.test(f.path)).sort((a, b) => a.path.localeCompare(b.path));
|
|
588
|
+
if (htmlAny.length > 0) return htmlAny[0].path;
|
|
589
|
+
return DEFAULT_ENTRYPOINT;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/components/static-uploader.tsx
|
|
593
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
594
|
+
function StaticUploader({ initial, onFilesReady, onClear }) {
|
|
595
|
+
const t = useT();
|
|
596
|
+
const [pending, setPending] = useState2(null);
|
|
597
|
+
const [issues, setIssues] = useState2([]);
|
|
598
|
+
const [busy, setBusy] = useState2(false);
|
|
599
|
+
const [error, setError] = useState2(null);
|
|
600
|
+
async function handleZip(file) {
|
|
601
|
+
setBusy(true);
|
|
602
|
+
setError(null);
|
|
603
|
+
try {
|
|
604
|
+
const { files, issues: structuralIssues } = await extractZip(file);
|
|
605
|
+
if (files.length === 0) {
|
|
606
|
+
setError(t("posts.form.static.emptyBundle"));
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const contentIssues = validateBundle(files);
|
|
610
|
+
const all = [...structuralIssues, ...contentIssues];
|
|
611
|
+
setPending(files);
|
|
612
|
+
setIssues(all);
|
|
613
|
+
if (all.length === 0) {
|
|
614
|
+
onFilesReady(files, guessEntrypoint(files));
|
|
615
|
+
}
|
|
616
|
+
} catch (e) {
|
|
617
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
618
|
+
} finally {
|
|
619
|
+
setBusy(false);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async function handleLooseFiles(files) {
|
|
623
|
+
setBusy(true);
|
|
624
|
+
setError(null);
|
|
625
|
+
try {
|
|
626
|
+
const extracted = [];
|
|
627
|
+
const structural = [];
|
|
628
|
+
for (const f of Array.from(files)) {
|
|
629
|
+
const rel = f.webkitRelativePath ?? f.name;
|
|
630
|
+
const stripped = rel.includes("/") ? rel.slice(rel.indexOf("/") + 1) : rel;
|
|
631
|
+
const reason = validateBundlePath(stripped);
|
|
632
|
+
if (reason) {
|
|
633
|
+
structural.push({ path: stripped, reason });
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
const buf = new Uint8Array(await f.arrayBuffer());
|
|
637
|
+
extracted.push({ path: stripped, data: buf });
|
|
638
|
+
}
|
|
639
|
+
if (extracted.length === 0) {
|
|
640
|
+
setError(t("posts.form.static.emptyBundle"));
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const content = validateBundle(extracted);
|
|
644
|
+
const all = [...structural, ...content];
|
|
645
|
+
setPending(extracted);
|
|
646
|
+
setIssues(all);
|
|
647
|
+
if (all.length === 0) {
|
|
648
|
+
onFilesReady(extracted, guessEntrypoint(extracted));
|
|
649
|
+
}
|
|
650
|
+
} catch (e) {
|
|
651
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
652
|
+
} finally {
|
|
653
|
+
setBusy(false);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function onPickerChange(e) {
|
|
657
|
+
const fl = e.target.files;
|
|
658
|
+
if (!fl || fl.length === 0) return;
|
|
659
|
+
e.target.value = "";
|
|
660
|
+
if (fl.length === 1 && fl[0].name.toLowerCase().endsWith(".zip")) {
|
|
661
|
+
void handleZip(fl[0]);
|
|
662
|
+
} else {
|
|
663
|
+
void handleLooseFiles(fl);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function clearPending() {
|
|
667
|
+
setPending(null);
|
|
668
|
+
setIssues([]);
|
|
669
|
+
setError(null);
|
|
670
|
+
onClear();
|
|
671
|
+
}
|
|
672
|
+
const showCurrent = !pending && initial && initial.files.length > 0;
|
|
673
|
+
return /* @__PURE__ */ jsxs5("div", { className: "space-y-4", children: [
|
|
674
|
+
/* @__PURE__ */ jsxs5("div", { className: "rounded-md border border-dashed p-4", children: [
|
|
675
|
+
/* @__PURE__ */ jsxs5("label", { className: "flex flex-col items-start gap-2 text-sm", children: [
|
|
676
|
+
/* @__PURE__ */ jsx5("span", { className: "font-medium", children: t("posts.form.static.pick") }),
|
|
677
|
+
/* @__PURE__ */ jsx5(
|
|
678
|
+
"input",
|
|
679
|
+
{
|
|
680
|
+
type: "file",
|
|
681
|
+
accept: ".zip,application/zip,*/*",
|
|
682
|
+
multiple: true,
|
|
683
|
+
onChange: onPickerChange,
|
|
684
|
+
disabled: busy
|
|
685
|
+
}
|
|
686
|
+
),
|
|
687
|
+
/* @__PURE__ */ jsx5("span", { className: "text-xs text-muted-foreground", children: t("posts.form.static.pickHint") })
|
|
688
|
+
] }),
|
|
689
|
+
busy && /* @__PURE__ */ jsx5("p", { className: "mt-2 text-sm text-muted-foreground", children: t("common.loading") }),
|
|
690
|
+
error && /* @__PURE__ */ jsx5("p", { className: "mt-2 text-sm text-destructive", children: error })
|
|
691
|
+
] }),
|
|
692
|
+
showCurrent && /* @__PURE__ */ jsx5(CurrentBundle, { body: initial }),
|
|
693
|
+
pending && /* @__PURE__ */ jsx5(
|
|
694
|
+
PendingBundle,
|
|
695
|
+
{
|
|
696
|
+
files: pending,
|
|
697
|
+
issues,
|
|
698
|
+
onClear: clearPending
|
|
699
|
+
}
|
|
700
|
+
)
|
|
701
|
+
] });
|
|
702
|
+
}
|
|
703
|
+
function CurrentBundle({ body }) {
|
|
704
|
+
const t = useT();
|
|
705
|
+
return /* @__PURE__ */ jsxs5("div", { className: "rounded-md border bg-muted/30 p-3", children: [
|
|
706
|
+
/* @__PURE__ */ jsxs5("div", { className: "mb-2 flex items-center gap-2 text-sm font-medium", children: [
|
|
707
|
+
/* @__PURE__ */ jsx5(FileArchive, { className: "h-4 w-4" }),
|
|
708
|
+
t("posts.form.static.currentBundle", {
|
|
709
|
+
count: body.files.length,
|
|
710
|
+
entrypoint: body.entrypoint
|
|
711
|
+
})
|
|
712
|
+
] }),
|
|
713
|
+
/* @__PURE__ */ jsx5(FileList, { files: body.files })
|
|
714
|
+
] });
|
|
715
|
+
}
|
|
716
|
+
function PendingBundle({
|
|
717
|
+
files,
|
|
718
|
+
issues,
|
|
719
|
+
onClear
|
|
720
|
+
}) {
|
|
721
|
+
const t = useT();
|
|
722
|
+
const totalBytes = files.reduce((sum, f) => sum + f.data.byteLength, 0);
|
|
723
|
+
return /* @__PURE__ */ jsxs5("div", { className: "rounded-md border p-3", children: [
|
|
724
|
+
/* @__PURE__ */ jsxs5("div", { className: "mb-2 flex items-center justify-between gap-2 text-sm font-medium", children: [
|
|
725
|
+
/* @__PURE__ */ jsxs5("span", { className: "flex items-center gap-2", children: [
|
|
726
|
+
/* @__PURE__ */ jsx5(FileArchive, { className: "h-4 w-4" }),
|
|
727
|
+
t("posts.form.static.pendingBundle", {
|
|
728
|
+
count: files.length,
|
|
729
|
+
size: formatBytes(totalBytes)
|
|
730
|
+
})
|
|
731
|
+
] }),
|
|
732
|
+
/* @__PURE__ */ jsxs5(Button4, { type: "button", variant: "ghost", size: "sm", onClick: onClear, children: [
|
|
733
|
+
/* @__PURE__ */ jsx5(X, { className: "mr-1 h-3 w-3" }),
|
|
734
|
+
t("common.cancel")
|
|
735
|
+
] })
|
|
736
|
+
] }),
|
|
737
|
+
issues.length > 0 && /* @__PURE__ */ jsxs5("div", { className: "mb-3 rounded-md border border-destructive/40 bg-destructive/5 p-2 text-sm", children: [
|
|
738
|
+
/* @__PURE__ */ jsxs5("div", { className: "mb-1 flex items-center gap-2 font-medium text-destructive", children: [
|
|
739
|
+
/* @__PURE__ */ jsx5(AlertTriangle, { className: "h-4 w-4" }),
|
|
740
|
+
t("posts.form.static.issuesTitle", { count: issues.length })
|
|
741
|
+
] }),
|
|
742
|
+
/* @__PURE__ */ jsxs5("ul", { className: "space-y-0.5 text-xs", children: [
|
|
743
|
+
issues.slice(0, 20).map((issue, idx) => /* @__PURE__ */ jsxs5("li", { className: "font-mono", children: [
|
|
744
|
+
issue.path,
|
|
745
|
+
": ",
|
|
746
|
+
issue.reason
|
|
747
|
+
] }, `${issue.path}-${idx}`)),
|
|
748
|
+
issues.length > 20 && /* @__PURE__ */ jsxs5("li", { className: "font-mono text-muted-foreground", children: [
|
|
749
|
+
"\u2026 ",
|
|
750
|
+
issues.length - 20,
|
|
751
|
+
" more"
|
|
752
|
+
] })
|
|
753
|
+
] }),
|
|
754
|
+
/* @__PURE__ */ jsx5("p", { className: "mt-2 text-xs text-muted-foreground", children: t("posts.form.static.issuesHint") })
|
|
755
|
+
] }),
|
|
756
|
+
/* @__PURE__ */ jsx5(FileList, { files: files.map((f) => f.path) })
|
|
757
|
+
] });
|
|
758
|
+
}
|
|
759
|
+
function FileList({ files }) {
|
|
760
|
+
return /* @__PURE__ */ jsxs5("ul", { className: "space-y-0.5 text-xs", children: [
|
|
761
|
+
files.slice(0, 40).map((path) => /* @__PURE__ */ jsxs5("li", { className: "flex items-center gap-1.5 font-mono text-muted-foreground", children: [
|
|
762
|
+
/* @__PURE__ */ jsx5(FileText, { className: "h-3 w-3 shrink-0" }),
|
|
763
|
+
path
|
|
764
|
+
] }, path)),
|
|
765
|
+
files.length > 40 && /* @__PURE__ */ jsxs5("li", { className: "font-mono text-xs text-muted-foreground", children: [
|
|
766
|
+
"\u2026 ",
|
|
767
|
+
files.length - 40,
|
|
768
|
+
" more"
|
|
769
|
+
] })
|
|
770
|
+
] });
|
|
771
|
+
}
|
|
772
|
+
function formatBytes(bytes) {
|
|
773
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
774
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
775
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
776
|
+
}
|
|
777
|
+
function guessEntrypoint(files) {
|
|
778
|
+
const exact = files.find((f) => f.path === "index.html");
|
|
779
|
+
if (exact) return "index.html";
|
|
780
|
+
const alt = files.find((f) => f.path === "index.htm");
|
|
781
|
+
if (alt) return "index.htm";
|
|
782
|
+
const rootHtml = files.filter((f) => /^[^/]+\.html?$/.test(f.path)).sort((a, b) => a.path.localeCompare(b.path))[0];
|
|
783
|
+
if (rootHtml) return rootHtml.path;
|
|
784
|
+
return files[0]?.path ?? "index.html";
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// src/components/post-form.tsx
|
|
788
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
789
|
+
var EMPTY_TIPTAP_DOC = { type: "doc", content: [{ type: "paragraph" }] };
|
|
790
|
+
var IMAGE_URL_RE = /\.(jpe?g|png|gif|webp|avif|svg|bmp|tiff?)(\?|$)/i;
|
|
791
|
+
var STYLESHEET_URL_RE = /\.css(\?|$)/i;
|
|
792
|
+
var SCRIPT_URL_RE = /\.m?js(\?|$)/i;
|
|
793
|
+
function snippetFor(url, format) {
|
|
794
|
+
const isImage = IMAGE_URL_RE.test(url);
|
|
795
|
+
if (format === "markdown") {
|
|
796
|
+
return isImage ? `` : url;
|
|
797
|
+
}
|
|
798
|
+
if (isImage) return `<img src="${url}" alt="" />`;
|
|
799
|
+
if (STYLESHEET_URL_RE.test(url)) return `<link rel="stylesheet" href="${url}" />`;
|
|
800
|
+
if (SCRIPT_URL_RE.test(url)) return `<script src="${url}"></script>`;
|
|
801
|
+
return url;
|
|
802
|
+
}
|
|
803
|
+
function slugify(s) {
|
|
804
|
+
return s.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-");
|
|
805
|
+
}
|
|
806
|
+
function defaultBodyForFormat(format) {
|
|
807
|
+
if (format === "tiptap") return EMPTY_TIPTAP_DOC;
|
|
808
|
+
if (format === "static") return null;
|
|
809
|
+
return "";
|
|
810
|
+
}
|
|
811
|
+
function isStaticBody(value) {
|
|
812
|
+
return !!value && typeof value === "object" && "entrypoint" in value && "files" in value && Array.isArray(value.files);
|
|
813
|
+
}
|
|
814
|
+
function PostForm({ post }) {
|
|
815
|
+
const router = useRouter();
|
|
816
|
+
const t = useT();
|
|
817
|
+
const isEdit = !!post;
|
|
818
|
+
const bodyTextareaRef = useRef2(null);
|
|
819
|
+
const [title, setTitle] = useState3(post?.title ?? "");
|
|
820
|
+
const [slug, setSlug] = useState3(post?.slug ?? "");
|
|
821
|
+
const [excerpt, setExcerpt] = useState3(post?.excerpt ?? "");
|
|
822
|
+
const [format, setFormat] = useState3(post?.format ?? "tiptap");
|
|
823
|
+
const [body, setBody] = useState3(post?.body ?? EMPTY_TIPTAP_DOC);
|
|
824
|
+
const [status, setStatus] = useState3(post?.status ?? "draft");
|
|
825
|
+
const [tagsInput, setTagsInput] = useState3((post?.tags ?? []).join(", "));
|
|
826
|
+
const [noLayout, setNoLayout] = useState3(post?.metadata?.no_layout === true);
|
|
827
|
+
const [saving, setSaving] = useState3(false);
|
|
828
|
+
const [error, setError] = useState3(null);
|
|
829
|
+
const [view, setView] = useState3("edit");
|
|
830
|
+
const [pendingBundle, setPendingBundle] = useState3(null);
|
|
831
|
+
const initialStaticBody = isStaticBody(post?.body) ? post.body : null;
|
|
832
|
+
function buildMetadata() {
|
|
833
|
+
const next = { ...post?.metadata ?? {} };
|
|
834
|
+
if (noLayout && format === "html") next.no_layout = true;
|
|
835
|
+
else delete next.no_layout;
|
|
836
|
+
return Object.keys(next).length > 0 ? next : void 0;
|
|
837
|
+
}
|
|
838
|
+
function parseTags(raw) {
|
|
839
|
+
return Array.from(
|
|
840
|
+
new Set(
|
|
841
|
+
raw.split(",").map((t2) => t2.trim()).filter(Boolean)
|
|
842
|
+
)
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
function insertMediaSnippet(url) {
|
|
846
|
+
if (format === "tiptap") return;
|
|
847
|
+
const snippet = snippetFor(url, format);
|
|
848
|
+
const ta = bodyTextareaRef.current;
|
|
849
|
+
const current = typeof body === "string" ? body : "";
|
|
850
|
+
if (!ta) {
|
|
851
|
+
setBody(current + snippet);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
const start = ta.selectionStart ?? current.length;
|
|
855
|
+
const end = ta.selectionEnd ?? current.length;
|
|
856
|
+
const next = current.slice(0, start) + snippet + current.slice(end);
|
|
857
|
+
setBody(next);
|
|
858
|
+
requestAnimationFrame(() => {
|
|
859
|
+
const t2 = bodyTextareaRef.current;
|
|
860
|
+
if (!t2) return;
|
|
861
|
+
t2.focus();
|
|
862
|
+
const pos = start + snippet.length;
|
|
863
|
+
t2.setSelectionRange(pos, pos);
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
function changeFormat(next) {
|
|
867
|
+
if (next === format) return;
|
|
868
|
+
let nextBody = body;
|
|
869
|
+
if (next === "static" || format === "static") {
|
|
870
|
+
nextBody = defaultBodyForFormat(next);
|
|
871
|
+
} else {
|
|
872
|
+
const k = `${format}\u2192${next}`;
|
|
873
|
+
switch (k) {
|
|
874
|
+
case "tiptap\u2192html":
|
|
875
|
+
nextBody = tiptapToHtml(body);
|
|
876
|
+
break;
|
|
877
|
+
case "tiptap\u2192markdown":
|
|
878
|
+
nextBody = tiptapToMarkdown(body);
|
|
879
|
+
break;
|
|
880
|
+
case "html\u2192tiptap":
|
|
881
|
+
nextBody = String(body ?? "");
|
|
882
|
+
break;
|
|
883
|
+
case "markdown\u2192tiptap":
|
|
884
|
+
nextBody = markdownToHtml(String(body ?? ""));
|
|
885
|
+
break;
|
|
886
|
+
case "html\u2192markdown":
|
|
887
|
+
nextBody = htmlToMarkdown(String(body ?? ""));
|
|
888
|
+
break;
|
|
889
|
+
case "markdown\u2192html":
|
|
890
|
+
nextBody = markdownToHtml(String(body ?? ""));
|
|
891
|
+
break;
|
|
892
|
+
default:
|
|
893
|
+
nextBody = defaultBodyForFormat(next);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
setFormat(next);
|
|
897
|
+
setBody(nextBody);
|
|
898
|
+
setPendingBundle(null);
|
|
899
|
+
if (next !== "html") setNoLayout(false);
|
|
900
|
+
}
|
|
901
|
+
async function save(e) {
|
|
902
|
+
e.preventDefault();
|
|
903
|
+
setSaving(true);
|
|
904
|
+
setError(null);
|
|
905
|
+
try {
|
|
906
|
+
const tags = parseTags(tagsInput);
|
|
907
|
+
const metadata = buildMetadata();
|
|
908
|
+
const finalSlug = slug || slugify(title);
|
|
909
|
+
const finalSiteId = post?.siteId ?? readAdminSiteIdFromCookie();
|
|
910
|
+
let nextBody = body;
|
|
911
|
+
if (format === "static") {
|
|
912
|
+
if (pendingBundle) {
|
|
913
|
+
nextBody = await uploadBundle({
|
|
914
|
+
siteId: finalSiteId,
|
|
915
|
+
slug: finalSlug,
|
|
916
|
+
files: pendingBundle.files,
|
|
917
|
+
entrypoint: pendingBundle.entrypoint
|
|
918
|
+
});
|
|
919
|
+
} else if (initialStaticBody) {
|
|
920
|
+
nextBody = initialStaticBody;
|
|
921
|
+
} else {
|
|
922
|
+
throw new Error(t("posts.form.static.noBundle"));
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (isEdit) {
|
|
926
|
+
await updatePost(
|
|
927
|
+
post.postId,
|
|
928
|
+
{
|
|
929
|
+
title,
|
|
930
|
+
slug: finalSlug,
|
|
931
|
+
excerpt: excerpt || void 0,
|
|
932
|
+
format,
|
|
933
|
+
body: nextBody,
|
|
934
|
+
status,
|
|
935
|
+
publishedAt: status === "published" ? post?.publishedAt ?? (/* @__PURE__ */ new Date()).toISOString() : void 0,
|
|
936
|
+
tags,
|
|
937
|
+
metadata
|
|
938
|
+
},
|
|
939
|
+
{ siteId: post.siteId }
|
|
940
|
+
);
|
|
941
|
+
} else {
|
|
942
|
+
await createPost({
|
|
943
|
+
siteId: finalSiteId,
|
|
944
|
+
slug: finalSlug,
|
|
945
|
+
title,
|
|
946
|
+
excerpt: excerpt || void 0,
|
|
947
|
+
format,
|
|
948
|
+
body: nextBody,
|
|
949
|
+
status,
|
|
950
|
+
publishedAt: status === "published" ? (/* @__PURE__ */ new Date()).toISOString() : void 0,
|
|
951
|
+
tags,
|
|
952
|
+
metadata
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
router.push("/admin/posts");
|
|
956
|
+
router.refresh();
|
|
957
|
+
} catch (err) {
|
|
958
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
959
|
+
} finally {
|
|
960
|
+
setSaving(false);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
async function handleDelete() {
|
|
964
|
+
if (!post) return;
|
|
965
|
+
if (!confirm(t("posts.form.deleteConfirm", { title: post.title }))) return;
|
|
966
|
+
setSaving(true);
|
|
967
|
+
try {
|
|
968
|
+
if (post.format === "static") {
|
|
969
|
+
await deleteBundle(post.siteId, post.slug).catch(() => void 0);
|
|
970
|
+
}
|
|
971
|
+
await deletePost(post.postId);
|
|
972
|
+
router.push("/admin/posts");
|
|
973
|
+
router.refresh();
|
|
974
|
+
} catch (err) {
|
|
975
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
976
|
+
setSaving(false);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
const previewPost = {
|
|
980
|
+
postId: post?.postId ?? "preview",
|
|
981
|
+
siteId: post?.siteId ?? readAdminSiteIdFromCookie(),
|
|
982
|
+
slug: slug || slugify(title) || "preview",
|
|
983
|
+
title,
|
|
984
|
+
excerpt: excerpt || void 0,
|
|
985
|
+
format,
|
|
986
|
+
body,
|
|
987
|
+
status,
|
|
988
|
+
publishedAt: status === "published" ? post?.publishedAt ?? (/* @__PURE__ */ new Date()).toISOString() : void 0,
|
|
989
|
+
tags: parseTags(tagsInput)
|
|
990
|
+
};
|
|
991
|
+
return /* @__PURE__ */ jsxs6("form", { onSubmit: save, className: "space-y-6", children: [
|
|
992
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex gap-1 border-b", children: [
|
|
993
|
+
/* @__PURE__ */ jsx6(
|
|
994
|
+
"button",
|
|
995
|
+
{
|
|
996
|
+
type: "button",
|
|
997
|
+
onClick: () => setView("edit"),
|
|
998
|
+
"aria-pressed": view === "edit",
|
|
999
|
+
className: `px-4 py-2 text-sm font-medium transition ${view === "edit" ? "border-b-2 border-[var(--primary)] text-[var(--primary)]" : "text-muted-foreground hover:text-foreground"}`,
|
|
1000
|
+
children: t("posts.form.tabEdit")
|
|
1001
|
+
}
|
|
1002
|
+
),
|
|
1003
|
+
/* @__PURE__ */ jsx6(
|
|
1004
|
+
"button",
|
|
1005
|
+
{
|
|
1006
|
+
type: "button",
|
|
1007
|
+
onClick: () => setView("preview"),
|
|
1008
|
+
"aria-pressed": view === "preview",
|
|
1009
|
+
className: `px-4 py-2 text-sm font-medium transition ${view === "preview" ? "border-b-2 border-[var(--primary)] text-[var(--primary)]" : "text-muted-foreground hover:text-foreground"}`,
|
|
1010
|
+
children: t("posts.form.tabPreview")
|
|
1011
|
+
}
|
|
1012
|
+
)
|
|
1013
|
+
] }),
|
|
1014
|
+
view === "preview" && /* @__PURE__ */ jsxs6("article", { className: "space-y-4", children: [
|
|
1015
|
+
/* @__PURE__ */ jsxs6("header", { className: "border-b pb-4", children: [
|
|
1016
|
+
/* @__PURE__ */ jsx6("h1", { className: "text-3xl font-bold tracking-tight", children: title || /* @__PURE__ */ jsx6("span", { className: "text-muted-foreground italic", children: t("posts.form.previewNoTitle") }) }),
|
|
1017
|
+
/* @__PURE__ */ jsxs6("p", { className: "mt-2 text-sm text-muted-foreground", children: [
|
|
1018
|
+
previewPost.publishedAt ? /* @__PURE__ */ jsx6("time", { dateTime: previewPost.publishedAt, children: formatDate(previewPost.publishedAt) }) : /* @__PURE__ */ jsx6("span", { children: t("common.draft") }),
|
|
1019
|
+
/* @__PURE__ */ jsx6("span", { className: "mx-2", children: "\xB7" }),
|
|
1020
|
+
/* @__PURE__ */ jsx6("span", { className: "font-mono text-xs uppercase", children: format })
|
|
1021
|
+
] }),
|
|
1022
|
+
excerpt && /* @__PURE__ */ jsx6("p", { className: "mt-3 text-base text-muted-foreground", children: excerpt })
|
|
1023
|
+
] }),
|
|
1024
|
+
format === "static" ? /* @__PURE__ */ jsx6("p", { className: "text-sm text-muted-foreground", children: t("posts.form.static.previewHint") }) : /* @__PURE__ */ jsx6(
|
|
1025
|
+
"div",
|
|
1026
|
+
{
|
|
1027
|
+
className: "prose prose-neutral dark:prose-invert max-w-none",
|
|
1028
|
+
dangerouslySetInnerHTML: { __html: renderBody(previewPost) }
|
|
1029
|
+
}
|
|
1030
|
+
),
|
|
1031
|
+
previewPost.tags && previewPost.tags.length > 0 && /* @__PURE__ */ jsx6("div", { className: "flex flex-wrap gap-2 border-t pt-4 text-sm", children: previewPost.tags.map((tag) => /* @__PURE__ */ jsxs6(
|
|
1032
|
+
"span",
|
|
1033
|
+
{
|
|
1034
|
+
className: "rounded-full border px-2 py-0.5 text-xs text-muted-foreground",
|
|
1035
|
+
children: [
|
|
1036
|
+
"#",
|
|
1037
|
+
tag
|
|
1038
|
+
]
|
|
1039
|
+
},
|
|
1040
|
+
tag
|
|
1041
|
+
)) }),
|
|
1042
|
+
/* @__PURE__ */ jsx6("p", { className: "text-xs text-muted-foreground", children: t("posts.form.previewHint") })
|
|
1043
|
+
] }),
|
|
1044
|
+
/* @__PURE__ */ jsxs6("div", { className: view === "edit" ? "space-y-6" : "hidden", children: [
|
|
1045
|
+
/* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
|
|
1046
|
+
/* @__PURE__ */ jsx6(Label, { htmlFor: "title", children: t("posts.form.title") }),
|
|
1047
|
+
/* @__PURE__ */ jsx6(
|
|
1048
|
+
Input,
|
|
1049
|
+
{
|
|
1050
|
+
id: "title",
|
|
1051
|
+
required: true,
|
|
1052
|
+
value: title,
|
|
1053
|
+
onChange: (e) => {
|
|
1054
|
+
setTitle(e.target.value);
|
|
1055
|
+
if (!isEdit && !slug) setSlug(slugify(e.target.value));
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
)
|
|
1059
|
+
] }),
|
|
1060
|
+
/* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
|
|
1061
|
+
/* @__PURE__ */ jsx6(Label, { htmlFor: "slug", children: t("posts.form.slug") }),
|
|
1062
|
+
/* @__PURE__ */ jsx6(
|
|
1063
|
+
Input,
|
|
1064
|
+
{
|
|
1065
|
+
id: "slug",
|
|
1066
|
+
value: slug,
|
|
1067
|
+
onChange: (e) => setSlug(e.target.value),
|
|
1068
|
+
placeholder: slugify(title) || t("posts.form.slugPlaceholder")
|
|
1069
|
+
}
|
|
1070
|
+
)
|
|
1071
|
+
] }),
|
|
1072
|
+
/* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
|
|
1073
|
+
/* @__PURE__ */ jsx6(Label, { htmlFor: "excerpt", children: t("posts.form.excerpt") }),
|
|
1074
|
+
/* @__PURE__ */ jsx6(
|
|
1075
|
+
Textarea,
|
|
1076
|
+
{
|
|
1077
|
+
id: "excerpt",
|
|
1078
|
+
rows: 2,
|
|
1079
|
+
value: excerpt,
|
|
1080
|
+
onChange: (e) => setExcerpt(e.target.value)
|
|
1081
|
+
}
|
|
1082
|
+
)
|
|
1083
|
+
] }),
|
|
1084
|
+
/* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
|
|
1085
|
+
/* @__PURE__ */ jsx6(Label, { htmlFor: "format", children: t("posts.form.format") }),
|
|
1086
|
+
/* @__PURE__ */ jsxs6(
|
|
1087
|
+
"select",
|
|
1088
|
+
{
|
|
1089
|
+
id: "format",
|
|
1090
|
+
value: format,
|
|
1091
|
+
onChange: (e) => changeFormat(e.target.value),
|
|
1092
|
+
className: "flex h-9 w-full max-w-xs rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm",
|
|
1093
|
+
children: [
|
|
1094
|
+
/* @__PURE__ */ jsx6("option", { value: "tiptap", children: "Tiptap (rich editor)" }),
|
|
1095
|
+
/* @__PURE__ */ jsx6("option", { value: "markdown", children: "Markdown" }),
|
|
1096
|
+
/* @__PURE__ */ jsx6("option", { value: "html", children: "HTML" }),
|
|
1097
|
+
/* @__PURE__ */ jsx6("option", { value: "static", children: t("posts.form.formatStaticLabel") })
|
|
1098
|
+
]
|
|
1099
|
+
}
|
|
1100
|
+
),
|
|
1101
|
+
/* @__PURE__ */ jsx6("p", { className: "text-xs text-muted-foreground", children: t("posts.form.formatHint") })
|
|
1102
|
+
] }),
|
|
1103
|
+
/* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
|
|
1104
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-between", children: [
|
|
1105
|
+
/* @__PURE__ */ jsx6(Label, { children: t("posts.form.body") }),
|
|
1106
|
+
format !== "tiptap" && format !== "static" && // For textarea-based formats (markdown / html) there's no
|
|
1107
|
+
// embedded toolbar, so we surface the MediaPicker as a
|
|
1108
|
+
// standalone button. Selecting an asset inserts a
|
|
1109
|
+
// format-aware snippet at the cursor.
|
|
1110
|
+
/* @__PURE__ */ jsx6(
|
|
1111
|
+
MediaPicker,
|
|
1112
|
+
{
|
|
1113
|
+
onSelect: insertMediaSnippet,
|
|
1114
|
+
trigger: /* @__PURE__ */ jsxs6(Button5, { type: "button", variant: "outline", size: "sm", children: [
|
|
1115
|
+
/* @__PURE__ */ jsx6(ImageIcon3, { className: "mr-2 h-3 w-3" }),
|
|
1116
|
+
t("posts.form.insertMedia")
|
|
1117
|
+
] })
|
|
1118
|
+
}
|
|
1119
|
+
)
|
|
1120
|
+
] }),
|
|
1121
|
+
format === "tiptap" ? /* @__PURE__ */ jsx6(TiptapEditor, { initialContent: body, onChange: setBody }) : format === "static" ? /* @__PURE__ */ jsx6(
|
|
1122
|
+
StaticUploader,
|
|
1123
|
+
{
|
|
1124
|
+
initial: initialStaticBody,
|
|
1125
|
+
onFilesReady: (files, entrypoint) => setPendingBundle({ files, entrypoint }),
|
|
1126
|
+
onClear: () => setPendingBundle(null)
|
|
1127
|
+
}
|
|
1128
|
+
) : /* @__PURE__ */ jsx6(
|
|
1129
|
+
Textarea,
|
|
1130
|
+
{
|
|
1131
|
+
ref: bodyTextareaRef,
|
|
1132
|
+
rows: 20,
|
|
1133
|
+
value: typeof body === "string" ? body : "",
|
|
1134
|
+
onChange: (e) => setBody(e.target.value),
|
|
1135
|
+
className: "font-mono text-xs"
|
|
1136
|
+
}
|
|
1137
|
+
)
|
|
1138
|
+
] }),
|
|
1139
|
+
/* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
|
|
1140
|
+
/* @__PURE__ */ jsx6(Label, { htmlFor: "tags", children: t("posts.form.tags") }),
|
|
1141
|
+
/* @__PURE__ */ jsx6(
|
|
1142
|
+
Input,
|
|
1143
|
+
{
|
|
1144
|
+
id: "tags",
|
|
1145
|
+
value: tagsInput,
|
|
1146
|
+
onChange: (e) => setTagsInput(e.target.value),
|
|
1147
|
+
placeholder: t("posts.form.tagsPlaceholder")
|
|
1148
|
+
}
|
|
1149
|
+
),
|
|
1150
|
+
/* @__PURE__ */ jsx6("p", { className: "text-xs text-muted-foreground", children: t("posts.form.tagsHint") })
|
|
1151
|
+
] }),
|
|
1152
|
+
/* @__PURE__ */ jsxs6("div", { className: "space-y-2", children: [
|
|
1153
|
+
/* @__PURE__ */ jsx6(Label, { htmlFor: "status", children: t("posts.form.status") }),
|
|
1154
|
+
/* @__PURE__ */ jsxs6(
|
|
1155
|
+
"select",
|
|
1156
|
+
{
|
|
1157
|
+
id: "status",
|
|
1158
|
+
value: status,
|
|
1159
|
+
onChange: (e) => setStatus(e.target.value),
|
|
1160
|
+
className: "flex h-9 w-full max-w-xs rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm",
|
|
1161
|
+
children: [
|
|
1162
|
+
/* @__PURE__ */ jsx6("option", { value: "draft", children: t("common.draft") }),
|
|
1163
|
+
/* @__PURE__ */ jsx6("option", { value: "published", children: t("common.published") })
|
|
1164
|
+
]
|
|
1165
|
+
}
|
|
1166
|
+
)
|
|
1167
|
+
] }),
|
|
1168
|
+
format === "html" && /* @__PURE__ */ jsx6("div", { className: "space-y-2", children: /* @__PURE__ */ jsxs6("label", { className: "flex items-start gap-2 text-sm", children: [
|
|
1169
|
+
/* @__PURE__ */ jsx6(
|
|
1170
|
+
"input",
|
|
1171
|
+
{
|
|
1172
|
+
type: "checkbox",
|
|
1173
|
+
checked: noLayout,
|
|
1174
|
+
onChange: (e) => setNoLayout(e.target.checked),
|
|
1175
|
+
className: "mt-1"
|
|
1176
|
+
}
|
|
1177
|
+
),
|
|
1178
|
+
/* @__PURE__ */ jsxs6("span", { children: [
|
|
1179
|
+
/* @__PURE__ */ jsx6("span", { className: "font-medium", children: t("posts.form.noLayout") }),
|
|
1180
|
+
/* @__PURE__ */ jsx6("span", { className: "block text-xs text-muted-foreground", children: t("posts.form.noLayoutHint") })
|
|
1181
|
+
] })
|
|
1182
|
+
] }) }),
|
|
1183
|
+
error && /* @__PURE__ */ jsx6("p", { className: "text-sm text-destructive", children: error }),
|
|
1184
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", children: [
|
|
1185
|
+
/* @__PURE__ */ jsx6(Button5, { type: "submit", disabled: saving, children: saving ? t("common.saving") : isEdit ? t("posts.form.saveChanges") : t("posts.form.createPost") }),
|
|
1186
|
+
isEdit && /* @__PURE__ */ jsx6(Button5, { type: "button", variant: "destructive", onClick: handleDelete, disabled: saving, children: t("posts.form.delete") })
|
|
1187
|
+
] })
|
|
1188
|
+
] })
|
|
1189
|
+
] });
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
export {
|
|
1193
|
+
sanitizeName,
|
|
1194
|
+
uploadProcessedImage,
|
|
1195
|
+
MediaPicker,
|
|
1196
|
+
PostForm
|
|
1197
|
+
};
|