@jiggai/kitchen-plugin-marketing 0.4.0 → 0.5.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/api/handler.js +179 -0
- package/dist/tabs/content-library.js +515 -106
- package/package.json +1 -1
package/dist/api/handler.js
CHANGED
|
@@ -1002,8 +1002,187 @@ async function handleRequest(req, ctx) {
|
|
|
1002
1002
|
return apiError(500, "DATABASE_ERROR", error?.message || "Unknown error");
|
|
1003
1003
|
}
|
|
1004
1004
|
}
|
|
1005
|
+
const MEDIA_DIR = (0, import_path2.join)((0, import_os2.homedir)(), ".openclaw", "kitchen", "plugins", "marketing", "media");
|
|
1006
|
+
function ensureMediaDir(team) {
|
|
1007
|
+
const dir = (0, import_path2.join)(MEDIA_DIR, team);
|
|
1008
|
+
(0, import_fs2.mkdirSync)(dir, { recursive: true });
|
|
1009
|
+
return dir;
|
|
1010
|
+
}
|
|
1011
|
+
if (req.path === "/media" && req.method === "POST") {
|
|
1012
|
+
try {
|
|
1013
|
+
const body = req.body;
|
|
1014
|
+
if (!body?.data) return apiError(400, "VALIDATION_ERROR", "data (base64) is required");
|
|
1015
|
+
let base64 = body.data;
|
|
1016
|
+
let detectedMime = body.mimeType || "application/octet-stream";
|
|
1017
|
+
const dataUrlMatch = base64.match(/^data:([^;]+);base64,(.+)$/);
|
|
1018
|
+
if (dataUrlMatch) {
|
|
1019
|
+
detectedMime = dataUrlMatch[1];
|
|
1020
|
+
base64 = dataUrlMatch[2];
|
|
1021
|
+
}
|
|
1022
|
+
const buf = Buffer.from(base64, "base64");
|
|
1023
|
+
const id = (0, import_crypto2.randomUUID)();
|
|
1024
|
+
const ext = (0, import_path2.extname)(body.filename || "") || mimeToExt(detectedMime);
|
|
1025
|
+
const storedFilename = `${id}${ext}`;
|
|
1026
|
+
const dir = ensureMediaDir(teamId);
|
|
1027
|
+
const filePath = (0, import_path2.join)(dir, storedFilename);
|
|
1028
|
+
(0, import_fs2.writeFileSync)(filePath, buf);
|
|
1029
|
+
const { db } = initializeDatabase(teamId);
|
|
1030
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1031
|
+
const userId = getUserId(req);
|
|
1032
|
+
const record = {
|
|
1033
|
+
id,
|
|
1034
|
+
teamId,
|
|
1035
|
+
filename: storedFilename,
|
|
1036
|
+
originalName: body.filename || storedFilename,
|
|
1037
|
+
mimeType: detectedMime,
|
|
1038
|
+
size: buf.length,
|
|
1039
|
+
width: null,
|
|
1040
|
+
height: null,
|
|
1041
|
+
alt: body.alt || null,
|
|
1042
|
+
tags: JSON.stringify(body.tags || []),
|
|
1043
|
+
url: `/api/plugins/marketing/media/${id}/file?team=${encodeURIComponent(teamId)}`,
|
|
1044
|
+
thumbnailUrl: null,
|
|
1045
|
+
createdAt: now,
|
|
1046
|
+
createdBy: userId
|
|
1047
|
+
};
|
|
1048
|
+
await db.insert(media).values(record);
|
|
1049
|
+
return {
|
|
1050
|
+
status: 201,
|
|
1051
|
+
data: {
|
|
1052
|
+
id,
|
|
1053
|
+
filename: record.originalName,
|
|
1054
|
+
mimeType: detectedMime,
|
|
1055
|
+
size: buf.length,
|
|
1056
|
+
url: record.url,
|
|
1057
|
+
alt: record.alt,
|
|
1058
|
+
tags: body.tags || [],
|
|
1059
|
+
createdAt: now
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
return apiError(500, "UPLOAD_ERROR", error?.message || "Upload failed");
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (req.path === "/media" && req.method === "GET") {
|
|
1067
|
+
try {
|
|
1068
|
+
const { db } = initializeDatabase(teamId);
|
|
1069
|
+
const { limit, offset } = parsePagination(req.query);
|
|
1070
|
+
const conditions = [(0, import_drizzle_orm2.eq)(media.teamId, teamId)];
|
|
1071
|
+
if (req.query.mimeType) {
|
|
1072
|
+
conditions.push((0, import_drizzle_orm2.like)(media.mimeType, `${req.query.mimeType}%`));
|
|
1073
|
+
}
|
|
1074
|
+
const totalResult = await db.select({ count: import_drizzle_orm2.sql`count(*)` }).from(media).where((0, import_drizzle_orm2.and)(...conditions));
|
|
1075
|
+
const total = totalResult[0]?.count ?? 0;
|
|
1076
|
+
const items = await db.select().from(media).where((0, import_drizzle_orm2.and)(...conditions)).orderBy((0, import_drizzle_orm2.desc)(media.createdAt)).limit(limit).offset(offset);
|
|
1077
|
+
const data = items.map((m) => {
|
|
1078
|
+
let thumbnailDataUrl;
|
|
1079
|
+
if (m.mimeType.startsWith("image/")) {
|
|
1080
|
+
const fp = (0, import_path2.join)(MEDIA_DIR, teamId, m.filename);
|
|
1081
|
+
if ((0, import_fs2.existsSync)(fp)) {
|
|
1082
|
+
const raw = (0, import_fs2.readFileSync)(fp);
|
|
1083
|
+
if (raw.length < 2 * 1024 * 1024) {
|
|
1084
|
+
thumbnailDataUrl = `data:${m.mimeType};base64,${raw.toString("base64")}`;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return {
|
|
1089
|
+
id: m.id,
|
|
1090
|
+
filename: m.originalName,
|
|
1091
|
+
mimeType: m.mimeType,
|
|
1092
|
+
size: m.size,
|
|
1093
|
+
url: m.url,
|
|
1094
|
+
thumbnailDataUrl,
|
|
1095
|
+
alt: m.alt,
|
|
1096
|
+
tags: JSON.parse(m.tags || "[]"),
|
|
1097
|
+
createdAt: m.createdAt
|
|
1098
|
+
};
|
|
1099
|
+
});
|
|
1100
|
+
return {
|
|
1101
|
+
status: 200,
|
|
1102
|
+
data: { data, total, offset, limit, hasMore: offset + limit < total }
|
|
1103
|
+
};
|
|
1104
|
+
} catch (error) {
|
|
1105
|
+
return apiError(500, "DATABASE_ERROR", error?.message || "Failed to list media");
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const mediaIdMatch = req.path.match(/^\/media\/([a-f0-9-]+)$/);
|
|
1109
|
+
if (mediaIdMatch && req.method === "GET") {
|
|
1110
|
+
try {
|
|
1111
|
+
const { db } = initializeDatabase(teamId);
|
|
1112
|
+
const [item] = await db.select().from(media).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(media.id, mediaIdMatch[1]), (0, import_drizzle_orm2.eq)(media.teamId, teamId)));
|
|
1113
|
+
if (!item) return apiError(404, "NOT_FOUND", "Media not found");
|
|
1114
|
+
let dataUrl;
|
|
1115
|
+
const fp = (0, import_path2.join)(MEDIA_DIR, teamId, item.filename);
|
|
1116
|
+
if ((0, import_fs2.existsSync)(fp)) {
|
|
1117
|
+
const raw = (0, import_fs2.readFileSync)(fp);
|
|
1118
|
+
dataUrl = `data:${item.mimeType};base64,${raw.toString("base64")}`;
|
|
1119
|
+
}
|
|
1120
|
+
return {
|
|
1121
|
+
status: 200,
|
|
1122
|
+
data: {
|
|
1123
|
+
id: item.id,
|
|
1124
|
+
filename: item.originalName,
|
|
1125
|
+
mimeType: item.mimeType,
|
|
1126
|
+
size: item.size,
|
|
1127
|
+
url: item.url,
|
|
1128
|
+
dataUrl,
|
|
1129
|
+
alt: item.alt,
|
|
1130
|
+
tags: JSON.parse(item.tags || "[]"),
|
|
1131
|
+
createdAt: item.createdAt
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
return apiError(500, "DATABASE_ERROR", error?.message || "Failed to get media");
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
const mediaFileMatch = req.path.match(/^\/media\/([a-f0-9-]+)\/file$/);
|
|
1139
|
+
if (mediaFileMatch && req.method === "GET") {
|
|
1140
|
+
try {
|
|
1141
|
+
const { db } = initializeDatabase(teamId);
|
|
1142
|
+
const [item] = await db.select().from(media).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(media.id, mediaFileMatch[1]), (0, import_drizzle_orm2.eq)(media.teamId, teamId)));
|
|
1143
|
+
if (!item) return apiError(404, "NOT_FOUND", "Media not found");
|
|
1144
|
+
const fp = (0, import_path2.join)(MEDIA_DIR, teamId, item.filename);
|
|
1145
|
+
if (!(0, import_fs2.existsSync)(fp)) return apiError(404, "NOT_FOUND", "File missing from disk");
|
|
1146
|
+
const raw = (0, import_fs2.readFileSync)(fp);
|
|
1147
|
+
const dataUrl = `data:${item.mimeType};base64,${raw.toString("base64")}`;
|
|
1148
|
+
return { status: 200, data: { dataUrl, mimeType: item.mimeType, filename: item.originalName } };
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
return apiError(500, "FILE_ERROR", error?.message || "Failed to serve file");
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
if (mediaIdMatch && req.method === "DELETE") {
|
|
1154
|
+
try {
|
|
1155
|
+
const { db } = initializeDatabase(teamId);
|
|
1156
|
+
const [item] = await db.select().from(media).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(media.id, mediaIdMatch[1]), (0, import_drizzle_orm2.eq)(media.teamId, teamId)));
|
|
1157
|
+
if (!item) return apiError(404, "NOT_FOUND", "Media not found");
|
|
1158
|
+
const fp = (0, import_path2.join)(MEDIA_DIR, teamId, item.filename);
|
|
1159
|
+
try {
|
|
1160
|
+
(0, import_fs2.unlinkSync)(fp);
|
|
1161
|
+
} catch {
|
|
1162
|
+
}
|
|
1163
|
+
await db.delete(media).where((0, import_drizzle_orm2.eq)(media.id, mediaIdMatch[1]));
|
|
1164
|
+
return { status: 200, data: { deleted: true, id: mediaIdMatch[1] } };
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
return apiError(500, "DATABASE_ERROR", error?.message || "Failed to delete media");
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1005
1169
|
return apiError(501, "NOT_IMPLEMENTED", `No handler for ${req.method} ${req.path}`);
|
|
1006
1170
|
}
|
|
1171
|
+
function mimeToExt(mime) {
|
|
1172
|
+
const map = {
|
|
1173
|
+
"image/jpeg": ".jpg",
|
|
1174
|
+
"image/png": ".png",
|
|
1175
|
+
"image/gif": ".gif",
|
|
1176
|
+
"image/webp": ".webp",
|
|
1177
|
+
"image/svg+xml": ".svg",
|
|
1178
|
+
"video/mp4": ".mp4",
|
|
1179
|
+
"video/webm": ".webm",
|
|
1180
|
+
"video/quicktime": ".mov",
|
|
1181
|
+
"audio/mpeg": ".mp3",
|
|
1182
|
+
"audio/wav": ".wav"
|
|
1183
|
+
};
|
|
1184
|
+
return map[mime] || "";
|
|
1185
|
+
}
|
|
1007
1186
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1008
1187
|
0 && (module.exports = {
|
|
1009
1188
|
handleRequest
|
|
@@ -120,6 +120,11 @@
|
|
|
120
120
|
const [scheduledAt, setScheduledAt] = useState("");
|
|
121
121
|
const [mediaUrl, setMediaUrl] = useState("");
|
|
122
122
|
const [showMedia, setShowMedia] = useState(false);
|
|
123
|
+
const [mediaLibrary, setMediaLibrary] = useState([]);
|
|
124
|
+
const [showMediaPicker, setShowMediaPicker] = useState(false);
|
|
125
|
+
const [uploading, setUploading] = useState(false);
|
|
126
|
+
const [selectedMediaIds, setSelectedMediaIds] = useState([]);
|
|
127
|
+
const fileInputRef = useRef(null);
|
|
123
128
|
const successTimeout = useRef(null);
|
|
124
129
|
const postizHeaders = useMemo(() => {
|
|
125
130
|
try {
|
|
@@ -154,10 +159,64 @@
|
|
|
154
159
|
} catch {
|
|
155
160
|
}
|
|
156
161
|
}, [apiBase, teamId]);
|
|
162
|
+
const loadMedia = useCallback(async () => {
|
|
163
|
+
try {
|
|
164
|
+
const res = await fetch(`${apiBase}/media?team=${encodeURIComponent(teamId)}&limit=100`);
|
|
165
|
+
const json = await res.json();
|
|
166
|
+
setMediaLibrary(Array.isArray(json.data) ? json.data : []);
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
}, [apiBase, teamId]);
|
|
170
|
+
const handleFileUpload = useCallback(async (files) => {
|
|
171
|
+
if (!files || files.length === 0) return;
|
|
172
|
+
setUploading(true);
|
|
173
|
+
setError(null);
|
|
174
|
+
try {
|
|
175
|
+
for (const file of Array.from(files)) {
|
|
176
|
+
const base64 = await new Promise((resolve, reject) => {
|
|
177
|
+
const reader = new FileReader();
|
|
178
|
+
reader.onload = () => resolve(reader.result);
|
|
179
|
+
reader.onerror = reject;
|
|
180
|
+
reader.readAsDataURL(file);
|
|
181
|
+
});
|
|
182
|
+
const res = await fetch(`${apiBase}/media?team=${encodeURIComponent(teamId)}`, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers: { "content-type": "application/json" },
|
|
185
|
+
body: JSON.stringify({ data: base64, filename: file.name, mimeType: file.type })
|
|
186
|
+
});
|
|
187
|
+
if (!res.ok) {
|
|
188
|
+
const err = await res.json().catch(() => ({}));
|
|
189
|
+
throw new Error(err.message || `Upload failed (${res.status})`);
|
|
190
|
+
}
|
|
191
|
+
const item = await res.json();
|
|
192
|
+
setSelectedMediaIds((prev) => [...prev, item.id]);
|
|
193
|
+
}
|
|
194
|
+
await loadMedia();
|
|
195
|
+
showSuccess(`Uploaded ${files.length} file${files.length > 1 ? "s" : ""}`);
|
|
196
|
+
} catch (e) {
|
|
197
|
+
setError(e?.message || "Upload failed");
|
|
198
|
+
} finally {
|
|
199
|
+
setUploading(false);
|
|
200
|
+
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
201
|
+
}
|
|
202
|
+
}, [apiBase, teamId, loadMedia]);
|
|
203
|
+
const deleteMedia = useCallback(async (id) => {
|
|
204
|
+
try {
|
|
205
|
+
await fetch(`${apiBase}/media/${id}?team=${encodeURIComponent(teamId)}`, { method: "DELETE" });
|
|
206
|
+
setSelectedMediaIds((prev) => prev.filter((x) => x !== id));
|
|
207
|
+
await loadMedia();
|
|
208
|
+
} catch {
|
|
209
|
+
}
|
|
210
|
+
}, [apiBase, teamId, loadMedia]);
|
|
211
|
+
const toggleMediaSelect = (id) => {
|
|
212
|
+
setSelectedMediaIds(
|
|
213
|
+
(prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
|
|
214
|
+
);
|
|
215
|
+
};
|
|
157
216
|
useEffect(() => {
|
|
158
217
|
setLoading(true);
|
|
159
|
-
Promise.all([loadDrivers(), loadPosts()]).finally(() => setLoading(false));
|
|
160
|
-
}, [loadDrivers, loadPosts]);
|
|
218
|
+
Promise.all([loadDrivers(), loadPosts(), loadMedia()]).finally(() => setLoading(false));
|
|
219
|
+
}, [loadDrivers, loadPosts, loadMedia]);
|
|
161
220
|
const connectedDrivers = useMemo(() => drivers.filter((d) => d.connected), [drivers]);
|
|
162
221
|
const disconnectedDrivers = useMemo(() => drivers.filter((d) => !d.connected), [drivers]);
|
|
163
222
|
const togglePlatform = (platform) => {
|
|
@@ -274,137 +333,487 @@
|
|
|
274
333
|
return h(
|
|
275
334
|
"div",
|
|
276
335
|
{ className: "space-y-3" },
|
|
277
|
-
// ---- Composer ----
|
|
336
|
+
// ---- Composer (two-column: compose left, preview right) ----
|
|
278
337
|
h(
|
|
279
338
|
"div",
|
|
280
339
|
{ style: t.card },
|
|
281
340
|
h("div", { className: "text-sm font-medium mb-3", style: t.text }, "Compose"),
|
|
282
341
|
h(
|
|
283
342
|
"div",
|
|
284
|
-
{
|
|
285
|
-
|
|
286
|
-
value: content,
|
|
287
|
-
onChange: (e) => setContent(e.target.value),
|
|
288
|
-
placeholder: "Write your post\u2026",
|
|
289
|
-
rows: 5,
|
|
290
|
-
style: { ...t.input, resize: "vertical", minHeight: "110px", fontFamily: "inherit" }
|
|
291
|
-
}),
|
|
292
|
-
// Character count
|
|
293
|
-
charLimit && content.length > 0 && h(
|
|
294
|
-
"div",
|
|
295
|
-
{ style: t.charWarn(content.length / charLimit * 100) },
|
|
296
|
-
`${content.length} / ${charLimit} characters`,
|
|
297
|
-
content.length > charLimit && " \u26A0 over limit"
|
|
298
|
-
),
|
|
299
|
-
!charLimit && content.length > 0 && h("div", { className: "text-xs", style: t.faint }, `${content.length} chars`),
|
|
300
|
-
// Platform selector — connected
|
|
343
|
+
{ style: { display: "flex", gap: "1rem" } },
|
|
344
|
+
// LEFT — compose pane
|
|
301
345
|
h(
|
|
302
346
|
"div",
|
|
303
|
-
|
|
304
|
-
h("
|
|
305
|
-
|
|
347
|
+
{ style: { flex: 1, minWidth: 0 }, className: "space-y-3" },
|
|
348
|
+
h("textarea", {
|
|
349
|
+
value: content,
|
|
350
|
+
onChange: (e) => setContent(e.target.value),
|
|
351
|
+
placeholder: "Write your post\u2026",
|
|
352
|
+
rows: 5,
|
|
353
|
+
style: { ...t.input, resize: "vertical", minHeight: "160px", fontFamily: "inherit" }
|
|
354
|
+
}),
|
|
355
|
+
// Character count
|
|
356
|
+
charLimit && content.length > 0 && h(
|
|
306
357
|
"div",
|
|
307
|
-
{
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
358
|
+
{ style: t.charWarn(content.length / charLimit * 100) },
|
|
359
|
+
`${content.length} / ${charLimit} characters`,
|
|
360
|
+
content.length > charLimit && " \u26A0 over limit"
|
|
361
|
+
),
|
|
362
|
+
!charLimit && content.length > 0 && h("div", { className: "text-xs", style: t.faint }, `${content.length} chars`),
|
|
363
|
+
// Platform selector — connected
|
|
364
|
+
h(
|
|
365
|
+
"div",
|
|
366
|
+
null,
|
|
367
|
+
h("div", { className: "text-xs font-medium mb-2", style: t.faint }, "Publish to"),
|
|
368
|
+
connectedDrivers.length > 0 ? h(
|
|
369
|
+
"div",
|
|
370
|
+
{ className: "flex flex-wrap gap-2" },
|
|
371
|
+
...connectedDrivers.map(
|
|
372
|
+
(d) => h(
|
|
373
|
+
"span",
|
|
374
|
+
{
|
|
375
|
+
key: d.platform,
|
|
376
|
+
onClick: () => togglePlatform(d.platform),
|
|
377
|
+
style: t.pill(selectedPlatforms.includes(d.platform), true),
|
|
378
|
+
role: "button",
|
|
379
|
+
tabIndex: 0,
|
|
380
|
+
title: `${d.displayName} via ${d.backend}`
|
|
381
|
+
},
|
|
382
|
+
`${d.icon} ${d.label}`,
|
|
383
|
+
h("span", { style: t.backendBadge(d.backend) }, d.backend)
|
|
384
|
+
)
|
|
385
|
+
),
|
|
386
|
+
...disconnectedDrivers.map(
|
|
387
|
+
(d) => h("span", {
|
|
312
388
|
key: d.platform,
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
389
|
+
style: t.pill(false, false),
|
|
390
|
+
title: `${d.label} \u2014 not connected`
|
|
391
|
+
}, `${d.icon} ${d.label}`)
|
|
392
|
+
)
|
|
393
|
+
) : h(
|
|
394
|
+
"div",
|
|
395
|
+
{ className: "flex flex-wrap gap-2" },
|
|
396
|
+
...drivers.map(
|
|
397
|
+
(d) => h(
|
|
398
|
+
"span",
|
|
399
|
+
{ key: d.platform, style: t.pill(false, false), title: "Not connected" },
|
|
400
|
+
`${d.icon} ${d.label}`
|
|
401
|
+
)
|
|
402
|
+
),
|
|
403
|
+
h(
|
|
404
|
+
"div",
|
|
405
|
+
{ className: "text-xs mt-1", style: t.faint },
|
|
406
|
+
"No platforms connected. Go to Accounts tab to set up Postiz or add accounts."
|
|
321
407
|
)
|
|
322
|
-
),
|
|
323
|
-
...disconnectedDrivers.map(
|
|
324
|
-
(d) => h("span", {
|
|
325
|
-
key: d.platform,
|
|
326
|
-
style: t.pill(false, false),
|
|
327
|
-
title: `${d.label} \u2014 not connected`
|
|
328
|
-
}, `${d.icon} ${d.label}`)
|
|
329
408
|
)
|
|
330
|
-
)
|
|
409
|
+
),
|
|
410
|
+
// Media (upload, URL, or library picker)
|
|
411
|
+
h(
|
|
331
412
|
"div",
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
413
|
+
null,
|
|
414
|
+
h("button", {
|
|
415
|
+
type: "button",
|
|
416
|
+
onClick: () => setShowMedia(!showMedia),
|
|
417
|
+
style: { ...t.btnGhost, padding: "0.3rem 0.55rem", fontSize: "0.8rem" }
|
|
418
|
+
}, showMedia ? "\u2212 Media" : "+ Media"),
|
|
419
|
+
showMedia && h(
|
|
420
|
+
"div",
|
|
421
|
+
{ className: "mt-2 space-y-2" },
|
|
422
|
+
// Upload + URL row
|
|
423
|
+
h(
|
|
424
|
+
"div",
|
|
425
|
+
{ className: "flex gap-2 items-center" },
|
|
426
|
+
h("input", {
|
|
427
|
+
ref: fileInputRef,
|
|
428
|
+
type: "file",
|
|
429
|
+
accept: "image/*,video/*",
|
|
430
|
+
multiple: true,
|
|
431
|
+
style: { display: "none" },
|
|
432
|
+
onChange: (e) => handleFileUpload(e.target.files)
|
|
433
|
+
}),
|
|
434
|
+
h("button", {
|
|
435
|
+
type: "button",
|
|
436
|
+
onClick: () => fileInputRef.current?.click(),
|
|
437
|
+
style: { ...t.btnGhost, padding: "0.35rem 0.7rem", fontSize: "0.8rem", whiteSpace: "nowrap" },
|
|
438
|
+
disabled: uploading
|
|
439
|
+
}, uploading ? "\u23F3 Uploading\u2026" : "\u{1F4C1} Upload"),
|
|
440
|
+
h("button", {
|
|
441
|
+
type: "button",
|
|
442
|
+
onClick: () => {
|
|
443
|
+
loadMedia();
|
|
444
|
+
setShowMediaPicker(!showMediaPicker);
|
|
445
|
+
},
|
|
446
|
+
style: { ...t.btnGhost, padding: "0.35rem 0.7rem", fontSize: "0.8rem", whiteSpace: "nowrap" }
|
|
447
|
+
}, showMediaPicker ? "Hide Library" : "\u{1F5BC}\uFE0F Library"),
|
|
448
|
+
h("input", {
|
|
449
|
+
type: "url",
|
|
450
|
+
value: mediaUrl,
|
|
451
|
+
onChange: (e) => setMediaUrl(e.target.value),
|
|
452
|
+
placeholder: "\u2026or paste a URL",
|
|
453
|
+
style: { ...t.input, flex: 1 }
|
|
454
|
+
})
|
|
455
|
+
),
|
|
456
|
+
// Selected media thumbnails
|
|
457
|
+
selectedMediaIds.length > 0 && h(
|
|
458
|
+
"div",
|
|
459
|
+
{ className: "flex flex-wrap gap-2" },
|
|
460
|
+
...selectedMediaIds.map((id) => {
|
|
461
|
+
const item = mediaLibrary.find((m) => m.id === id);
|
|
462
|
+
if (!item) return null;
|
|
463
|
+
return h(
|
|
464
|
+
"div",
|
|
465
|
+
{
|
|
466
|
+
key: id,
|
|
467
|
+
style: {
|
|
468
|
+
position: "relative",
|
|
469
|
+
width: "72px",
|
|
470
|
+
height: "72px",
|
|
471
|
+
borderRadius: "8px",
|
|
472
|
+
overflow: "hidden",
|
|
473
|
+
border: "2px solid rgba(127,90,240,0.5)"
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
item.mimeType?.startsWith("video/") ? h("div", {
|
|
477
|
+
style: {
|
|
478
|
+
width: "100%",
|
|
479
|
+
height: "100%",
|
|
480
|
+
background: "rgba(0,0,0,0.4)",
|
|
481
|
+
display: "flex",
|
|
482
|
+
alignItems: "center",
|
|
483
|
+
justifyContent: "center",
|
|
484
|
+
color: "white",
|
|
485
|
+
fontSize: "1.2rem"
|
|
486
|
+
}
|
|
487
|
+
}, "\u{1F3A5}") : h("img", {
|
|
488
|
+
src: item.thumbnailDataUrl || item.url,
|
|
489
|
+
style: { width: "100%", height: "100%", objectFit: "cover" }
|
|
490
|
+
}),
|
|
491
|
+
h("button", {
|
|
492
|
+
type: "button",
|
|
493
|
+
onClick: () => toggleMediaSelect(id),
|
|
494
|
+
style: {
|
|
495
|
+
position: "absolute",
|
|
496
|
+
top: "2px",
|
|
497
|
+
right: "2px",
|
|
498
|
+
background: "rgba(0,0,0,0.6)",
|
|
499
|
+
border: "none",
|
|
500
|
+
borderRadius: "50%",
|
|
501
|
+
width: "18px",
|
|
502
|
+
height: "18px",
|
|
503
|
+
color: "white",
|
|
504
|
+
fontSize: "0.65rem",
|
|
505
|
+
cursor: "pointer",
|
|
506
|
+
display: "flex",
|
|
507
|
+
alignItems: "center",
|
|
508
|
+
justifyContent: "center",
|
|
509
|
+
lineHeight: "1"
|
|
510
|
+
}
|
|
511
|
+
}, "\u2715")
|
|
512
|
+
);
|
|
513
|
+
})
|
|
514
|
+
),
|
|
515
|
+
// Media library picker grid
|
|
516
|
+
showMediaPicker && h(
|
|
517
|
+
"div",
|
|
518
|
+
{
|
|
519
|
+
style: {
|
|
520
|
+
background: "rgba(255,255,255,0.02)",
|
|
521
|
+
border: "1px solid var(--ck-border-subtle)",
|
|
522
|
+
borderRadius: "10px",
|
|
523
|
+
padding: "0.75rem",
|
|
524
|
+
maxHeight: "260px",
|
|
525
|
+
overflowY: "auto"
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
h(
|
|
529
|
+
"div",
|
|
530
|
+
{ className: "text-xs font-medium mb-2", style: t.faint },
|
|
531
|
+
`Media Library (${mediaLibrary.length} items)`
|
|
532
|
+
),
|
|
533
|
+
mediaLibrary.length === 0 ? h("div", { className: "text-xs py-4 text-center", style: t.faint }, "No media yet. Upload some files!") : h(
|
|
534
|
+
"div",
|
|
535
|
+
{
|
|
536
|
+
style: {
|
|
537
|
+
display: "grid",
|
|
538
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(80px, 1fr))",
|
|
539
|
+
gap: "0.5rem"
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
...mediaLibrary.map((item) => {
|
|
543
|
+
const isSelected = selectedMediaIds.includes(item.id);
|
|
544
|
+
return h(
|
|
545
|
+
"div",
|
|
546
|
+
{
|
|
547
|
+
key: item.id,
|
|
548
|
+
onClick: () => toggleMediaSelect(item.id),
|
|
549
|
+
style: {
|
|
550
|
+
position: "relative",
|
|
551
|
+
cursor: "pointer",
|
|
552
|
+
width: "100%",
|
|
553
|
+
paddingTop: "100%",
|
|
554
|
+
borderRadius: "8px",
|
|
555
|
+
overflow: "hidden",
|
|
556
|
+
border: isSelected ? "2px solid rgba(127,90,240,0.7)" : "1px solid var(--ck-border-subtle)",
|
|
557
|
+
boxShadow: isSelected ? "0 0 8px rgba(127,90,240,0.3)" : "none"
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
item.mimeType?.startsWith("video/") ? h("div", {
|
|
561
|
+
style: {
|
|
562
|
+
position: "absolute",
|
|
563
|
+
inset: "0",
|
|
564
|
+
background: "rgba(0,0,0,0.4)",
|
|
565
|
+
display: "flex",
|
|
566
|
+
alignItems: "center",
|
|
567
|
+
justifyContent: "center",
|
|
568
|
+
color: "white",
|
|
569
|
+
fontSize: "1.5rem"
|
|
570
|
+
}
|
|
571
|
+
}, "\u{1F3A5}") : h("img", {
|
|
572
|
+
src: item.thumbnailDataUrl || item.url,
|
|
573
|
+
style: {
|
|
574
|
+
position: "absolute",
|
|
575
|
+
inset: "0",
|
|
576
|
+
width: "100%",
|
|
577
|
+
height: "100%",
|
|
578
|
+
objectFit: "cover"
|
|
579
|
+
}
|
|
580
|
+
}),
|
|
581
|
+
isSelected && h("div", {
|
|
582
|
+
style: {
|
|
583
|
+
position: "absolute",
|
|
584
|
+
top: "4px",
|
|
585
|
+
right: "4px",
|
|
586
|
+
background: "rgba(127,90,240,0.85)",
|
|
587
|
+
borderRadius: "50%",
|
|
588
|
+
width: "20px",
|
|
589
|
+
height: "20px",
|
|
590
|
+
display: "flex",
|
|
591
|
+
alignItems: "center",
|
|
592
|
+
justifyContent: "center",
|
|
593
|
+
color: "white",
|
|
594
|
+
fontSize: "0.7rem",
|
|
595
|
+
fontWeight: "700"
|
|
596
|
+
}
|
|
597
|
+
}, "\u2713"),
|
|
598
|
+
h("button", {
|
|
599
|
+
type: "button",
|
|
600
|
+
onClick: (e) => {
|
|
601
|
+
e.stopPropagation();
|
|
602
|
+
deleteMedia(item.id);
|
|
603
|
+
},
|
|
604
|
+
style: {
|
|
605
|
+
position: "absolute",
|
|
606
|
+
bottom: "4px",
|
|
607
|
+
right: "4px",
|
|
608
|
+
background: "rgba(220,38,38,0.7)",
|
|
609
|
+
border: "none",
|
|
610
|
+
borderRadius: "50%",
|
|
611
|
+
width: "18px",
|
|
612
|
+
height: "18px",
|
|
613
|
+
color: "white",
|
|
614
|
+
fontSize: "0.6rem",
|
|
615
|
+
cursor: "pointer",
|
|
616
|
+
display: "flex",
|
|
617
|
+
alignItems: "center",
|
|
618
|
+
justifyContent: "center",
|
|
619
|
+
opacity: "0.6",
|
|
620
|
+
lineHeight: "1"
|
|
621
|
+
},
|
|
622
|
+
title: "Delete from library"
|
|
623
|
+
}, "\u{1F5D1}"),
|
|
624
|
+
h("div", {
|
|
625
|
+
style: {
|
|
626
|
+
position: "absolute",
|
|
627
|
+
bottom: "0",
|
|
628
|
+
left: "0",
|
|
629
|
+
right: "0",
|
|
630
|
+
background: "rgba(0,0,0,0.6)",
|
|
631
|
+
padding: "2px 4px",
|
|
632
|
+
fontSize: "0.55rem",
|
|
633
|
+
color: "rgba(255,255,255,0.8)",
|
|
634
|
+
whiteSpace: "nowrap",
|
|
635
|
+
overflow: "hidden",
|
|
636
|
+
textOverflow: "ellipsis"
|
|
637
|
+
}
|
|
638
|
+
}, item.filename)
|
|
639
|
+
);
|
|
640
|
+
})
|
|
641
|
+
)
|
|
338
642
|
)
|
|
339
|
-
)
|
|
643
|
+
)
|
|
644
|
+
),
|
|
645
|
+
// Schedule (only if any selected platform supports it)
|
|
646
|
+
(canSchedule || !hasSelection) && h(
|
|
647
|
+
"div",
|
|
648
|
+
{ className: "grid grid-cols-1 gap-2 sm:grid-cols-2" },
|
|
340
649
|
h(
|
|
341
650
|
"div",
|
|
342
|
-
|
|
343
|
-
|
|
651
|
+
null,
|
|
652
|
+
h(
|
|
653
|
+
"div",
|
|
654
|
+
{ className: "text-xs font-medium mb-1", style: t.faint },
|
|
655
|
+
canSchedule ? "Schedule (optional)" : "Schedule (connect Postiz for scheduling)"
|
|
656
|
+
),
|
|
657
|
+
h("input", {
|
|
658
|
+
type: "datetime-local",
|
|
659
|
+
value: scheduledAt,
|
|
660
|
+
onChange: (e) => setScheduledAt(e.target.value),
|
|
661
|
+
style: { ...t.input, opacity: canSchedule || !hasSelection ? 1 : 0.5 },
|
|
662
|
+
disabled: hasSelection && !canSchedule
|
|
663
|
+
})
|
|
344
664
|
)
|
|
345
|
-
)
|
|
665
|
+
),
|
|
666
|
+
// Actions
|
|
667
|
+
h(
|
|
668
|
+
"div",
|
|
669
|
+
{ className: "flex flex-wrap gap-2 items-center" },
|
|
670
|
+
h("button", {
|
|
671
|
+
type: "button",
|
|
672
|
+
onClick: () => void onSaveDraft(),
|
|
673
|
+
style: { ...t.btnGhost, opacity: saving ? 0.7 : 1 },
|
|
674
|
+
disabled: saving || !content.trim()
|
|
675
|
+
}, saving ? "Saving\u2026" : "Save draft"),
|
|
676
|
+
hasConnected && hasSelection && h("button", {
|
|
677
|
+
type: "button",
|
|
678
|
+
onClick: () => void onPublish(),
|
|
679
|
+
style: { ...t.btnPublish, opacity: publishing ? 0.7 : 1 },
|
|
680
|
+
disabled: publishing || !content.trim()
|
|
681
|
+
}, publishing ? "Publishing\u2026" : scheduledAt ? "\u23F1 Schedule" : "\u{1F4E4} Publish now")
|
|
682
|
+
),
|
|
683
|
+
error && h("div", { className: "text-xs", style: { color: "rgba(248,113,113,0.95)" } }, error),
|
|
684
|
+
success && h("div", { className: "text-xs", style: { color: "rgba(74,222,128,0.9)" } }, success)
|
|
346
685
|
),
|
|
347
|
-
//
|
|
686
|
+
// RIGHT — live preview pane
|
|
348
687
|
h(
|
|
349
688
|
"div",
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
689
|
+
{
|
|
690
|
+
style: {
|
|
691
|
+
width: "320px",
|
|
692
|
+
flexShrink: 0,
|
|
693
|
+
background: "rgba(255,255,255,0.02)",
|
|
694
|
+
border: "1px solid var(--ck-border-subtle)",
|
|
695
|
+
borderRadius: "10px",
|
|
696
|
+
padding: "1rem",
|
|
697
|
+
display: "flex",
|
|
698
|
+
flexDirection: "column",
|
|
699
|
+
alignSelf: "flex-start"
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
h("div", { className: "text-sm font-medium mb-3", style: t.text }, "Post Preview"),
|
|
703
|
+
// Selected platforms
|
|
704
|
+
selectedPlatforms.length > 0 && h(
|
|
357
705
|
"div",
|
|
358
|
-
{ className: "
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
706
|
+
{ className: "flex flex-wrap gap-1 mb-3" },
|
|
707
|
+
...selectedPlatforms.map((pl) => {
|
|
708
|
+
const drv = drivers.find((d) => d.platform === pl);
|
|
709
|
+
return h("span", {
|
|
710
|
+
key: pl,
|
|
711
|
+
style: {
|
|
712
|
+
background: "rgba(127,90,240,0.15)",
|
|
713
|
+
border: "1px solid rgba(127,90,240,0.3)",
|
|
714
|
+
borderRadius: "999px",
|
|
715
|
+
padding: "0.12rem 0.45rem",
|
|
716
|
+
fontSize: "0.72rem",
|
|
717
|
+
color: "var(--ck-text-secondary)"
|
|
718
|
+
}
|
|
719
|
+
}, drv ? `${drv.icon} ${drv.label}` : pl);
|
|
365
720
|
})
|
|
366
|
-
)
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
721
|
+
),
|
|
722
|
+
// Scheduling info
|
|
723
|
+
scheduledAt && h("div", {
|
|
724
|
+
className: "text-xs mb-3",
|
|
725
|
+
style: { color: "rgba(251,191,36,0.85)" }
|
|
726
|
+
}, `\u23F1 Scheduled: ${new Date(scheduledAt).toLocaleString()}`),
|
|
727
|
+
// Content preview
|
|
728
|
+
content.trim() ? h("div", {
|
|
729
|
+
style: {
|
|
730
|
+
background: "rgba(255,255,255,0.03)",
|
|
731
|
+
border: "1px solid var(--ck-border-subtle)",
|
|
732
|
+
borderRadius: "10px",
|
|
733
|
+
padding: "0.85rem",
|
|
734
|
+
whiteSpace: "pre-wrap",
|
|
735
|
+
fontSize: "0.85rem",
|
|
736
|
+
color: "var(--ck-text-primary)",
|
|
737
|
+
lineHeight: "1.55",
|
|
738
|
+
maxHeight: "300px",
|
|
739
|
+
overflowY: "auto",
|
|
740
|
+
wordBreak: "break-word"
|
|
741
|
+
}
|
|
742
|
+
}, content) : h("div", {
|
|
743
|
+
style: {
|
|
744
|
+
color: "var(--ck-text-tertiary)",
|
|
745
|
+
fontSize: "0.85rem",
|
|
746
|
+
fontStyle: "italic",
|
|
747
|
+
padding: "2rem 0.5rem",
|
|
748
|
+
textAlign: "center"
|
|
749
|
+
}
|
|
750
|
+
}, "Start writing to see a preview"),
|
|
751
|
+
// Media preview (selected library items + URL)
|
|
752
|
+
(selectedMediaIds.length > 0 || mediaUrl && showMedia) && h(
|
|
373
753
|
"div",
|
|
374
|
-
|
|
754
|
+
{ className: "mt-3 space-y-2" },
|
|
755
|
+
...selectedMediaIds.map((id) => {
|
|
756
|
+
const item = mediaLibrary.find((m) => m.id === id);
|
|
757
|
+
if (!item) return null;
|
|
758
|
+
return item.mimeType?.startsWith("video/") ? h("div", {
|
|
759
|
+
key: id,
|
|
760
|
+
style: {
|
|
761
|
+
background: "rgba(0,0,0,0.3)",
|
|
762
|
+
borderRadius: "8px",
|
|
763
|
+
border: "1px solid var(--ck-border-subtle)",
|
|
764
|
+
padding: "1rem",
|
|
765
|
+
textAlign: "center",
|
|
766
|
+
color: "var(--ck-text-secondary)",
|
|
767
|
+
fontSize: "0.8rem"
|
|
768
|
+
}
|
|
769
|
+
}, `\u{1F3A5} ${item.filename}`) : h("img", {
|
|
770
|
+
key: id,
|
|
771
|
+
src: item.thumbnailDataUrl || item.url,
|
|
772
|
+
style: {
|
|
773
|
+
maxWidth: "100%",
|
|
774
|
+
borderRadius: "8px",
|
|
775
|
+
border: "1px solid var(--ck-border-subtle)"
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
}),
|
|
779
|
+
mediaUrl && showMedia && h("img", {
|
|
780
|
+
src: mediaUrl,
|
|
781
|
+
style: {
|
|
782
|
+
maxWidth: "100%",
|
|
783
|
+
borderRadius: "8px",
|
|
784
|
+
border: "1px solid var(--ck-border-subtle)"
|
|
785
|
+
},
|
|
786
|
+
onError: (e) => {
|
|
787
|
+
e.target.style.display = "none";
|
|
788
|
+
}
|
|
789
|
+
})
|
|
790
|
+
),
|
|
791
|
+
// Character limit bar
|
|
792
|
+
charLimit && content.length > 0 && h(
|
|
793
|
+
"div",
|
|
794
|
+
{ className: "mt-3" },
|
|
375
795
|
h(
|
|
376
796
|
"div",
|
|
377
|
-
{
|
|
378
|
-
|
|
797
|
+
{ style: { height: "4px", borderRadius: "2px", background: "rgba(255,255,255,0.06)", overflow: "hidden" } },
|
|
798
|
+
h("div", {
|
|
799
|
+
style: {
|
|
800
|
+
height: "100%",
|
|
801
|
+
borderRadius: "2px",
|
|
802
|
+
width: `${Math.min(content.length / charLimit * 100, 100)}%`,
|
|
803
|
+
background: content.length > charLimit ? "rgba(248,113,113,0.8)" : content.length > charLimit * 0.9 ? "rgba(251,191,36,0.8)" : "rgba(127,90,240,0.6)",
|
|
804
|
+
transition: "width 0.2s, background 0.2s"
|
|
805
|
+
}
|
|
806
|
+
})
|
|
379
807
|
),
|
|
380
|
-
h("
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
})
|
|
808
|
+
h("div", {
|
|
809
|
+
className: "text-xs mt-1",
|
|
810
|
+
style: {
|
|
811
|
+
color: content.length > charLimit ? "rgba(248,113,113,0.9)" : "var(--ck-text-tertiary)",
|
|
812
|
+
textAlign: "right"
|
|
813
|
+
}
|
|
814
|
+
}, `${content.length} / ${charLimit}`)
|
|
387
815
|
)
|
|
388
|
-
)
|
|
389
|
-
// Actions
|
|
390
|
-
h(
|
|
391
|
-
"div",
|
|
392
|
-
{ className: "flex flex-wrap gap-2 items-center" },
|
|
393
|
-
h("button", {
|
|
394
|
-
type: "button",
|
|
395
|
-
onClick: () => void onSaveDraft(),
|
|
396
|
-
style: { ...t.btnGhost, opacity: saving ? 0.7 : 1 },
|
|
397
|
-
disabled: saving || !content.trim()
|
|
398
|
-
}, saving ? "Saving\u2026" : "Save draft"),
|
|
399
|
-
hasConnected && hasSelection && h("button", {
|
|
400
|
-
type: "button",
|
|
401
|
-
onClick: () => void onPublish(),
|
|
402
|
-
style: { ...t.btnPublish, opacity: publishing ? 0.7 : 1 },
|
|
403
|
-
disabled: publishing || !content.trim()
|
|
404
|
-
}, publishing ? "Publishing\u2026" : scheduledAt ? "\u23F1 Schedule" : "\u{1F4E4} Publish now")
|
|
405
|
-
),
|
|
406
|
-
error && h("div", { className: "text-xs", style: { color: "rgba(248,113,113,0.95)" } }, error),
|
|
407
|
-
success && h("div", { className: "text-xs", style: { color: "rgba(74,222,128,0.9)" } }, success)
|
|
816
|
+
)
|
|
408
817
|
)
|
|
409
818
|
),
|
|
410
819
|
// ---- Posts list ----
|