@jiggai/kitchen-plugin-marketing 0.2.6 → 0.2.8
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 +134 -1
- package/dist/tabs/accounts.js +304 -94
- package/dist/tabs/content-library.js +194 -70
- package/package.json +1 -1
package/dist/api/handler.js
CHANGED
|
@@ -224,8 +224,141 @@ function getTeamId(req) {
|
|
|
224
224
|
function getUserId(req) {
|
|
225
225
|
return req.headers["x-user-id"] || "system";
|
|
226
226
|
}
|
|
227
|
-
|
|
227
|
+
function getPostizConfig(req) {
|
|
228
|
+
const apiKey = req.query.postizApiKey || req.headers["x-postiz-api-key"];
|
|
229
|
+
const baseUrl = req.query.postizBaseUrl || req.headers["x-postiz-base-url"] || "https://api.postiz.com/public/v1";
|
|
230
|
+
if (!apiKey) return null;
|
|
231
|
+
return { apiKey, baseUrl: baseUrl.replace(/\/+$/, "") };
|
|
232
|
+
}
|
|
233
|
+
async function postizFetch(config, path, options) {
|
|
234
|
+
return fetch(`${config.baseUrl}${path}`, {
|
|
235
|
+
...options,
|
|
236
|
+
headers: {
|
|
237
|
+
"Authorization": config.apiKey,
|
|
238
|
+
"Content-Type": "application/json",
|
|
239
|
+
...options?.headers || {}
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
async function detectProviders(req, teamId) {
|
|
244
|
+
const providers = [];
|
|
245
|
+
const postizCfg = getPostizConfig(req);
|
|
246
|
+
if (postizCfg) {
|
|
247
|
+
try {
|
|
248
|
+
const res = await postizFetch(postizCfg, "/integrations");
|
|
249
|
+
if (res.ok) {
|
|
250
|
+
const data = await res.json();
|
|
251
|
+
const integrations = Array.isArray(data) ? data : data.integrations || [];
|
|
252
|
+
for (const integ of integrations) {
|
|
253
|
+
providers.push({
|
|
254
|
+
id: `postiz:${integ.id}`,
|
|
255
|
+
type: "postiz",
|
|
256
|
+
platform: integ.providerIdentifier || integ.provider || "unknown",
|
|
257
|
+
displayName: integ.name || integ.providerIdentifier || "Postiz account",
|
|
258
|
+
username: integ.username || void 0,
|
|
259
|
+
avatar: integ.picture || integ.avatar || void 0,
|
|
260
|
+
isActive: !integ.disabled,
|
|
261
|
+
capabilities: ["post", "schedule"],
|
|
262
|
+
meta: { postizId: integ.id, provider: integ.providerIdentifier }
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
const fs = await import("fs");
|
|
271
|
+
const path = await import("path");
|
|
272
|
+
const os = await import("os");
|
|
273
|
+
const configPath = path.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
274
|
+
if (fs.existsSync(configPath)) {
|
|
275
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
276
|
+
const plugins = cfg?.plugins?.entries || {};
|
|
277
|
+
if (plugins.discord?.enabled) {
|
|
278
|
+
providers.push({
|
|
279
|
+
id: "gateway:discord",
|
|
280
|
+
type: "gateway",
|
|
281
|
+
platform: "discord",
|
|
282
|
+
displayName: "Discord (via OpenClaw)",
|
|
283
|
+
isActive: true,
|
|
284
|
+
capabilities: ["post"],
|
|
285
|
+
meta: { channel: "discord" }
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (plugins.telegram?.enabled) {
|
|
289
|
+
providers.push({
|
|
290
|
+
id: "gateway:telegram",
|
|
291
|
+
type: "gateway",
|
|
292
|
+
platform: "telegram",
|
|
293
|
+
displayName: "Telegram (via OpenClaw)",
|
|
294
|
+
isActive: true,
|
|
295
|
+
capabilities: ["post"],
|
|
296
|
+
meta: { channel: "telegram" }
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
}
|
|
302
|
+
return providers;
|
|
303
|
+
}
|
|
304
|
+
async function postizPublish(config, body) {
|
|
305
|
+
const payload = {
|
|
306
|
+
content: body.content,
|
|
307
|
+
integrationIds: body.integrationIds
|
|
308
|
+
};
|
|
309
|
+
if (body.scheduledAt) {
|
|
310
|
+
payload.date = body.scheduledAt;
|
|
311
|
+
}
|
|
312
|
+
if (body.settings) {
|
|
313
|
+
payload.settings = body.settings;
|
|
314
|
+
}
|
|
315
|
+
if (body.mediaUrls && body.mediaUrls.length > 0) {
|
|
316
|
+
payload.media = body.mediaUrls.map((url) => ({ url }));
|
|
317
|
+
}
|
|
318
|
+
const res = await postizFetch(config, "/posts", {
|
|
319
|
+
method: "POST",
|
|
320
|
+
body: JSON.stringify(payload)
|
|
321
|
+
});
|
|
322
|
+
const data = await res.json().catch(() => null);
|
|
323
|
+
if (!res.ok) {
|
|
324
|
+
return apiError(res.status, "POSTIZ_ERROR", data?.message || `Postiz returned ${res.status}`, data);
|
|
325
|
+
}
|
|
326
|
+
return { status: 201, data };
|
|
327
|
+
}
|
|
328
|
+
async function handleRequest(req, ctx) {
|
|
228
329
|
const teamId = getTeamId(req);
|
|
330
|
+
if (req.path === "/providers" && req.method === "GET") {
|
|
331
|
+
try {
|
|
332
|
+
const providers = await detectProviders(req, teamId);
|
|
333
|
+
return { status: 200, data: { providers } };
|
|
334
|
+
} catch (error) {
|
|
335
|
+
return apiError(500, "DETECT_ERROR", error?.message || "Failed to detect providers");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (req.path === "/providers/postiz/integrations" && req.method === "GET") {
|
|
339
|
+
const postizCfg = getPostizConfig(req);
|
|
340
|
+
if (!postizCfg) return apiError(400, "NO_POSTIZ", "Postiz API key not configured");
|
|
341
|
+
try {
|
|
342
|
+
const res = await postizFetch(postizCfg, "/integrations");
|
|
343
|
+
const data = await res.json();
|
|
344
|
+
return { status: res.status, data };
|
|
345
|
+
} catch (error) {
|
|
346
|
+
return apiError(502, "POSTIZ_UNREACHABLE", error?.message || "Cannot reach Postiz");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (req.path === "/publish" && req.method === "POST") {
|
|
350
|
+
const postizCfg = getPostizConfig(req);
|
|
351
|
+
if (!postizCfg) return apiError(400, "NO_POSTIZ", "Postiz API key not configured");
|
|
352
|
+
const body = req.body || {};
|
|
353
|
+
if (!body.content || !body.integrationIds?.length) {
|
|
354
|
+
return apiError(400, "VALIDATION_ERROR", "content and integrationIds are required");
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
return await postizPublish(postizCfg, body);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
return apiError(502, "POSTIZ_ERROR", error?.message || "Publish failed");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
229
362
|
if (req.path === "/posts" && req.method === "GET") {
|
|
230
363
|
try {
|
|
231
364
|
const { db } = initializeDatabase(teamId);
|
package/dist/tabs/accounts.js
CHANGED
|
@@ -29,85 +29,197 @@
|
|
|
29
29
|
background: "var(--ck-accent-red)",
|
|
30
30
|
border: "1px solid rgba(255,255,255,0.08)",
|
|
31
31
|
borderRadius: "10px",
|
|
32
|
-
padding: "0.
|
|
32
|
+
padding: "0.5rem 0.75rem",
|
|
33
33
|
color: "white",
|
|
34
34
|
fontWeight: 700,
|
|
35
|
-
cursor: "pointer"
|
|
35
|
+
cursor: "pointer",
|
|
36
|
+
fontSize: "0.8rem"
|
|
36
37
|
},
|
|
37
38
|
btnGhost: {
|
|
38
39
|
background: "rgba(255,255,255,0.03)",
|
|
39
40
|
border: "1px solid var(--ck-border-subtle)",
|
|
40
41
|
borderRadius: "10px",
|
|
41
|
-
padding: "0.
|
|
42
|
+
padding: "0.5rem 0.75rem",
|
|
42
43
|
color: "var(--ck-text-primary)",
|
|
43
44
|
fontWeight: 600,
|
|
44
|
-
cursor: "pointer"
|
|
45
|
-
|
|
45
|
+
cursor: "pointer",
|
|
46
|
+
fontSize: "0.8rem"
|
|
47
|
+
},
|
|
48
|
+
badge: (color) => ({
|
|
49
|
+
display: "inline-block",
|
|
50
|
+
background: color,
|
|
51
|
+
borderRadius: "999px",
|
|
52
|
+
padding: "0.15rem 0.5rem",
|
|
53
|
+
fontSize: "0.7rem",
|
|
54
|
+
fontWeight: 600,
|
|
55
|
+
color: "white"
|
|
56
|
+
})
|
|
57
|
+
};
|
|
58
|
+
const PLATFORM_ICONS = {
|
|
59
|
+
x: "\u{1D54F}",
|
|
60
|
+
twitter: "\u{1D54F}",
|
|
61
|
+
instagram: "\u{1F4F7}",
|
|
62
|
+
linkedin: "\u{1F4BC}",
|
|
63
|
+
facebook: "\u{1F4D8}",
|
|
64
|
+
youtube: "\u25B6\uFE0F",
|
|
65
|
+
tiktok: "\u{1F3B5}",
|
|
66
|
+
bluesky: "\u{1F98B}",
|
|
67
|
+
mastodon: "\u{1F418}",
|
|
68
|
+
reddit: "\u{1F916}",
|
|
69
|
+
discord: "\u{1F4AC}",
|
|
70
|
+
telegram: "\u2708\uFE0F",
|
|
71
|
+
pinterest: "\u{1F4CC}",
|
|
72
|
+
threads: "\u{1F9F5}",
|
|
73
|
+
medium: "\u270D\uFE0F",
|
|
74
|
+
wordpress: "\u{1F4DD}"
|
|
75
|
+
};
|
|
76
|
+
const TYPE_COLORS = {
|
|
77
|
+
postiz: "rgba(99,179,237,0.7)",
|
|
78
|
+
gateway: "rgba(134,239,172,0.7)",
|
|
79
|
+
skill: "rgba(251,191,36,0.7)",
|
|
80
|
+
manual: "rgba(167,139,250,0.7)"
|
|
46
81
|
};
|
|
47
82
|
function Accounts(props) {
|
|
48
83
|
const teamId = String(props?.teamId || "default");
|
|
49
84
|
const apiBase = useMemo(() => `/api/plugins/marketing`, []);
|
|
50
|
-
const [
|
|
85
|
+
const [providers, setProviders] = useState([]);
|
|
86
|
+
const [manualAccounts, setManualAccounts] = useState([]);
|
|
51
87
|
const [loading, setLoading] = useState(true);
|
|
52
|
-
const [
|
|
53
|
-
const [platform, setPlatform] = useState("twitter");
|
|
54
|
-
const [displayName, setDisplayName] = useState("");
|
|
55
|
-
const [username, setUsername] = useState("");
|
|
56
|
-
const [accessToken, setAccessToken] = useState("");
|
|
57
|
-
const [saving, setSaving] = useState(false);
|
|
88
|
+
const [detecting, setDetecting] = useState(false);
|
|
58
89
|
const [error, setError] = useState(null);
|
|
59
|
-
const
|
|
60
|
-
|
|
90
|
+
const [postizKey, setPostizKey] = useState("");
|
|
91
|
+
const [postizUrl, setPostizUrl] = useState("https://api.postiz.com/public/v1");
|
|
92
|
+
const [showPostizSetup, setShowPostizSetup] = useState(false);
|
|
93
|
+
const [showManual, setShowManual] = useState(false);
|
|
94
|
+
const [manPlatform, setManPlatform] = useState("twitter");
|
|
95
|
+
const [manName, setManName] = useState("");
|
|
96
|
+
const [manUser, setManUser] = useState("");
|
|
97
|
+
const [manToken, setManToken] = useState("");
|
|
98
|
+
const [saving, setSaving] = useState(false);
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
try {
|
|
101
|
+
const stored = localStorage.getItem(`ck-postiz-${teamId}`);
|
|
102
|
+
if (stored) {
|
|
103
|
+
const parsed = JSON.parse(stored);
|
|
104
|
+
setPostizKey(parsed.apiKey || "");
|
|
105
|
+
setPostizUrl(parsed.baseUrl || "https://api.postiz.com/public/v1");
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
}, [teamId]);
|
|
110
|
+
const savePostizConfig = () => {
|
|
111
|
+
try {
|
|
112
|
+
localStorage.setItem(`ck-postiz-${teamId}`, JSON.stringify({ apiKey: postizKey, baseUrl: postizUrl }));
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
setShowPostizSetup(false);
|
|
116
|
+
void detectAll();
|
|
117
|
+
};
|
|
118
|
+
const getStoredPostiz = () => {
|
|
119
|
+
try {
|
|
120
|
+
const stored = localStorage.getItem(`ck-postiz-${teamId}`);
|
|
121
|
+
if (stored) return JSON.parse(stored);
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
};
|
|
126
|
+
const detectAll = async () => {
|
|
127
|
+
setDetecting(true);
|
|
61
128
|
setError(null);
|
|
62
129
|
try {
|
|
63
|
-
const
|
|
130
|
+
const stored = getStoredPostiz();
|
|
131
|
+
const key = postizKey || stored?.apiKey || "";
|
|
132
|
+
const url = postizUrl || stored?.baseUrl || "https://api.postiz.com/public/v1";
|
|
133
|
+
const headers = {};
|
|
134
|
+
if (key) {
|
|
135
|
+
headers["x-postiz-api-key"] = key;
|
|
136
|
+
headers["x-postiz-base-url"] = url;
|
|
137
|
+
}
|
|
138
|
+
const res = await fetch(`${apiBase}/providers?team=${encodeURIComponent(teamId)}`, { headers });
|
|
64
139
|
const json = await res.json();
|
|
65
|
-
|
|
140
|
+
setProviders(Array.isArray(json.providers) ? json.providers : []);
|
|
66
141
|
} catch (e) {
|
|
67
|
-
setError(e?.message || "Failed to
|
|
142
|
+
setError(e?.message || "Failed to detect providers");
|
|
68
143
|
} finally {
|
|
69
|
-
|
|
144
|
+
setDetecting(false);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const loadManual = async () => {
|
|
148
|
+
try {
|
|
149
|
+
const res = await fetch(`${apiBase}/accounts?team=${encodeURIComponent(teamId)}`);
|
|
150
|
+
const json = await res.json();
|
|
151
|
+
setManualAccounts(Array.isArray(json.accounts) ? json.accounts : []);
|
|
152
|
+
} catch {
|
|
70
153
|
}
|
|
71
154
|
};
|
|
155
|
+
const refresh = async () => {
|
|
156
|
+
setLoading(true);
|
|
157
|
+
await Promise.all([detectAll(), loadManual()]);
|
|
158
|
+
setLoading(false);
|
|
159
|
+
};
|
|
72
160
|
useEffect(() => {
|
|
73
161
|
void refresh();
|
|
74
162
|
}, [teamId]);
|
|
75
|
-
const
|
|
163
|
+
const onManualConnect = async () => {
|
|
76
164
|
setSaving(true);
|
|
77
165
|
setError(null);
|
|
78
166
|
try {
|
|
79
|
-
const res = await fetch(
|
|
80
|
-
|
|
81
|
-
{
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
throw new Error(err?.message || `Connect failed (${res.status})`);
|
|
96
|
-
}
|
|
97
|
-
setOpen(false);
|
|
98
|
-
setDisplayName("");
|
|
99
|
-
setUsername("");
|
|
100
|
-
setAccessToken("");
|
|
101
|
-
await refresh();
|
|
167
|
+
const res = await fetch(`${apiBase}/accounts?team=${encodeURIComponent(teamId)}`, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: { "content-type": "application/json" },
|
|
170
|
+
body: JSON.stringify({
|
|
171
|
+
platform: manPlatform,
|
|
172
|
+
displayName: manName || `${manPlatform} account`,
|
|
173
|
+
username: manUser || void 0,
|
|
174
|
+
credentials: { accessToken: manToken }
|
|
175
|
+
})
|
|
176
|
+
});
|
|
177
|
+
if (!res.ok) throw new Error(`Failed (${res.status})`);
|
|
178
|
+
setShowManual(false);
|
|
179
|
+
setManName("");
|
|
180
|
+
setManUser("");
|
|
181
|
+
setManToken("");
|
|
182
|
+
await loadManual();
|
|
102
183
|
} catch (e) {
|
|
103
|
-
setError(e?.message || "Failed to connect
|
|
184
|
+
setError(e?.message || "Failed to connect");
|
|
104
185
|
} finally {
|
|
105
186
|
setSaving(false);
|
|
106
187
|
}
|
|
107
188
|
};
|
|
189
|
+
const allProviders = useMemo(() => {
|
|
190
|
+
const combined = [...providers];
|
|
191
|
+
for (const ma of manualAccounts) {
|
|
192
|
+
combined.push({
|
|
193
|
+
id: `manual:${ma.id}`,
|
|
194
|
+
type: "manual",
|
|
195
|
+
platform: ma.platform,
|
|
196
|
+
displayName: ma.displayName,
|
|
197
|
+
username: ma.username,
|
|
198
|
+
isActive: ma.isActive,
|
|
199
|
+
capabilities: ["post"]
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return combined;
|
|
203
|
+
}, [providers, manualAccounts]);
|
|
204
|
+
const grouped = useMemo(() => {
|
|
205
|
+
const g = {};
|
|
206
|
+
for (const p of allProviders) {
|
|
207
|
+
const key = p.type;
|
|
208
|
+
if (!g[key]) g[key] = [];
|
|
209
|
+
g[key].push(p);
|
|
210
|
+
}
|
|
211
|
+
return g;
|
|
212
|
+
}, [allProviders]);
|
|
213
|
+
const typeLabels = {
|
|
214
|
+
postiz: "Postiz",
|
|
215
|
+
gateway: "OpenClaw Channels",
|
|
216
|
+
skill: "Skills",
|
|
217
|
+
manual: "Manual"
|
|
218
|
+
};
|
|
108
219
|
return h(
|
|
109
220
|
"div",
|
|
110
221
|
{ className: "space-y-3" },
|
|
222
|
+
// ---- Header ----
|
|
111
223
|
h(
|
|
112
224
|
"div",
|
|
113
225
|
{ style: t.card },
|
|
@@ -117,22 +229,81 @@
|
|
|
117
229
|
h(
|
|
118
230
|
"div",
|
|
119
231
|
null,
|
|
120
|
-
h("div", { className: "text-sm font-medium", style: t.text }, "Accounts"),
|
|
121
|
-
h(
|
|
232
|
+
h("div", { className: "text-sm font-medium", style: t.text }, "Connected Accounts"),
|
|
233
|
+
h(
|
|
234
|
+
"div",
|
|
235
|
+
{ className: "mt-1 text-xs", style: t.faint },
|
|
236
|
+
`${allProviders.length} provider${allProviders.length !== 1 ? "s" : ""} detected`
|
|
237
|
+
)
|
|
238
|
+
),
|
|
239
|
+
h(
|
|
240
|
+
"div",
|
|
241
|
+
{ className: "flex flex-wrap gap-2" },
|
|
242
|
+
h(
|
|
243
|
+
"button",
|
|
244
|
+
{ type: "button", onClick: () => void refresh(), style: t.btnGhost, disabled: detecting },
|
|
245
|
+
detecting ? "Detecting\u2026" : "\u21BB Refresh"
|
|
246
|
+
),
|
|
247
|
+
h(
|
|
248
|
+
"button",
|
|
249
|
+
{ type: "button", onClick: () => setShowPostizSetup(!showPostizSetup), style: t.btnGhost },
|
|
250
|
+
postizKey ? "\u2699 Postiz" : "+ Postiz"
|
|
251
|
+
),
|
|
252
|
+
h("button", { type: "button", onClick: () => setShowManual(!showManual), style: t.btnGhost }, "+ Manual")
|
|
253
|
+
)
|
|
254
|
+
),
|
|
255
|
+
error && h("div", { className: "mt-2 text-xs", style: { color: "rgba(248,113,113,0.95)" } }, error)
|
|
256
|
+
),
|
|
257
|
+
// ---- Postiz setup ----
|
|
258
|
+
showPostizSetup && h(
|
|
259
|
+
"div",
|
|
260
|
+
{ style: t.card },
|
|
261
|
+
h("div", { className: "text-sm font-medium mb-2", style: t.text }, "Postiz Configuration"),
|
|
262
|
+
h(
|
|
263
|
+
"div",
|
|
264
|
+
{ className: "text-xs mb-3", style: t.faint },
|
|
265
|
+
"Connect Postiz to manage social accounts via their platform. Get your API key from Postiz Settings \u2192 Developers \u2192 Public API."
|
|
266
|
+
),
|
|
267
|
+
h(
|
|
268
|
+
"div",
|
|
269
|
+
{ className: "grid grid-cols-1 gap-2 sm:grid-cols-2" },
|
|
270
|
+
h(
|
|
271
|
+
"div",
|
|
272
|
+
null,
|
|
273
|
+
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "API Key"),
|
|
274
|
+
h("input", {
|
|
275
|
+
type: "password",
|
|
276
|
+
value: postizKey,
|
|
277
|
+
onChange: (e) => setPostizKey(e.target.value),
|
|
278
|
+
placeholder: "your-postiz-api-key",
|
|
279
|
+
style: t.input
|
|
280
|
+
})
|
|
122
281
|
),
|
|
123
282
|
h(
|
|
124
283
|
"div",
|
|
125
|
-
|
|
126
|
-
h("
|
|
127
|
-
h("
|
|
284
|
+
null,
|
|
285
|
+
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "Base URL"),
|
|
286
|
+
h("input", {
|
|
287
|
+
value: postizUrl,
|
|
288
|
+
onChange: (e) => setPostizUrl(e.target.value),
|
|
289
|
+
placeholder: "https://api.postiz.com/public/v1",
|
|
290
|
+
style: t.input
|
|
291
|
+
})
|
|
128
292
|
)
|
|
129
293
|
),
|
|
130
|
-
|
|
294
|
+
h(
|
|
295
|
+
"div",
|
|
296
|
+
{ className: "mt-3 flex gap-2" },
|
|
297
|
+
h("button", { type: "button", onClick: () => setShowPostizSetup(false), style: t.btnGhost }, "Cancel"),
|
|
298
|
+
h("button", { type: "button", onClick: savePostizConfig, style: t.btnPrimary }, postizKey ? "Save & Detect" : "Save")
|
|
299
|
+
)
|
|
131
300
|
),
|
|
132
|
-
|
|
301
|
+
// ---- Manual account form ----
|
|
302
|
+
showManual && h(
|
|
133
303
|
"div",
|
|
134
304
|
{ style: t.card },
|
|
135
|
-
h("div", { className: "text-sm font-medium mb-
|
|
305
|
+
h("div", { className: "text-sm font-medium mb-2", style: t.text }, "Add manual account"),
|
|
306
|
+
h("div", { className: "text-xs mb-3", style: t.faint }, "For direct API access without Postiz. You provide the token."),
|
|
136
307
|
h(
|
|
137
308
|
"div",
|
|
138
309
|
{ className: "grid grid-cols-1 gap-2 sm:grid-cols-2" },
|
|
@@ -142,78 +313,117 @@
|
|
|
142
313
|
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "Platform"),
|
|
143
314
|
h(
|
|
144
315
|
"select",
|
|
145
|
-
{
|
|
146
|
-
value: platform,
|
|
147
|
-
onChange: (e) => setPlatform(e.target.value),
|
|
148
|
-
style: t.input
|
|
149
|
-
},
|
|
316
|
+
{ value: manPlatform, onChange: (e) => setManPlatform(e.target.value), style: t.input },
|
|
150
317
|
h("option", { value: "twitter" }, "Twitter / X"),
|
|
151
318
|
h("option", { value: "instagram" }, "Instagram"),
|
|
152
|
-
h("option", { value: "linkedin" }, "LinkedIn")
|
|
319
|
+
h("option", { value: "linkedin" }, "LinkedIn"),
|
|
320
|
+
h("option", { value: "bluesky" }, "Bluesky"),
|
|
321
|
+
h("option", { value: "mastodon" }, "Mastodon")
|
|
153
322
|
)
|
|
154
323
|
),
|
|
155
324
|
h(
|
|
156
325
|
"div",
|
|
157
326
|
null,
|
|
158
327
|
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "Display name"),
|
|
159
|
-
h("input", {
|
|
160
|
-
value: displayName,
|
|
161
|
-
onChange: (e) => setDisplayName(e.target.value),
|
|
162
|
-
placeholder: "e.g. RJ \u2014 Main",
|
|
163
|
-
style: t.input
|
|
164
|
-
})
|
|
328
|
+
h("input", { value: manName, onChange: (e) => setManName(e.target.value), placeholder: "My X account", style: t.input })
|
|
165
329
|
),
|
|
166
330
|
h(
|
|
167
331
|
"div",
|
|
168
332
|
null,
|
|
169
|
-
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "Username
|
|
170
|
-
h("input", {
|
|
171
|
-
value: username,
|
|
172
|
-
onChange: (e) => setUsername(e.target.value),
|
|
173
|
-
placeholder: "e.g. @handle",
|
|
174
|
-
style: t.input
|
|
175
|
-
})
|
|
333
|
+
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "Username"),
|
|
334
|
+
h("input", { value: manUser, onChange: (e) => setManUser(e.target.value), placeholder: "@handle", style: t.input })
|
|
176
335
|
),
|
|
177
336
|
h(
|
|
178
337
|
"div",
|
|
179
338
|
null,
|
|
180
|
-
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "Access token
|
|
181
|
-
h("input", {
|
|
182
|
-
value: accessToken,
|
|
183
|
-
onChange: (e) => setAccessToken(e.target.value),
|
|
184
|
-
placeholder: "token\u2026",
|
|
185
|
-
style: t.input
|
|
186
|
-
})
|
|
339
|
+
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "Access token"),
|
|
340
|
+
h("input", { type: "password", value: manToken, onChange: (e) => setManToken(e.target.value), placeholder: "token\u2026", style: t.input })
|
|
187
341
|
)
|
|
188
342
|
),
|
|
189
343
|
h(
|
|
190
344
|
"div",
|
|
191
345
|
{ className: "mt-3 flex gap-2" },
|
|
192
|
-
h("button", { type: "button", onClick: () =>
|
|
193
|
-
h("button", { type: "button", onClick: () => void
|
|
346
|
+
h("button", { type: "button", onClick: () => setShowManual(false), style: t.btnGhost }, "Cancel"),
|
|
347
|
+
h("button", { type: "button", onClick: () => void onManualConnect(), style: t.btnPrimary, disabled: saving }, saving ? "Saving\u2026" : "Connect")
|
|
194
348
|
)
|
|
195
|
-
)
|
|
196
|
-
|
|
349
|
+
),
|
|
350
|
+
// ---- Loading ----
|
|
351
|
+
loading && h(
|
|
197
352
|
"div",
|
|
198
353
|
{ style: t.card },
|
|
199
|
-
h("div", { className: "
|
|
200
|
-
|
|
354
|
+
h("div", { className: "py-6 text-center text-sm", style: t.faint }, "Detecting providers\u2026")
|
|
355
|
+
),
|
|
356
|
+
// ---- Provider groups ----
|
|
357
|
+
!loading && allProviders.length === 0 && h(
|
|
358
|
+
"div",
|
|
359
|
+
{ style: t.card },
|
|
360
|
+
h(
|
|
201
361
|
"div",
|
|
202
|
-
{ className: "space-y-2" },
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
362
|
+
{ className: "py-6 text-center space-y-2" },
|
|
363
|
+
h("div", { className: "text-sm", style: t.faint }, "No providers detected"),
|
|
364
|
+
h(
|
|
365
|
+
"div",
|
|
366
|
+
{ className: "text-xs", style: t.faint },
|
|
367
|
+
"Connect Postiz for full social media management, or add accounts manually."
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
),
|
|
371
|
+
!loading && Object.entries(grouped).map(
|
|
372
|
+
([type, items]) => h(
|
|
373
|
+
"div",
|
|
374
|
+
{ key: type, style: t.card },
|
|
375
|
+
h(
|
|
376
|
+
"div",
|
|
377
|
+
{ className: "flex items-center gap-2 mb-3" },
|
|
378
|
+
h("div", { className: "text-sm font-medium", style: t.text }, typeLabels[type] || type),
|
|
379
|
+
h("span", { style: t.badge(TYPE_COLORS[type] || "rgba(100,100,100,0.6)") }, `${items.length}`)
|
|
380
|
+
),
|
|
381
|
+
h(
|
|
382
|
+
"div",
|
|
383
|
+
{ className: "space-y-2" },
|
|
384
|
+
...items.map(
|
|
385
|
+
(p) => h(
|
|
208
386
|
"div",
|
|
209
|
-
{
|
|
387
|
+
{ key: p.id, style: { ...t.card, padding: "0.75rem", display: "flex", alignItems: "center", gap: "0.75rem" } },
|
|
388
|
+
// Avatar or platform icon
|
|
389
|
+
p.avatar ? h("img", { src: p.avatar, alt: "", style: { width: 32, height: 32, borderRadius: "50%", objectFit: "cover" } }) : h("div", {
|
|
390
|
+
style: {
|
|
391
|
+
width: 32,
|
|
392
|
+
height: 32,
|
|
393
|
+
borderRadius: "50%",
|
|
394
|
+
background: "rgba(255,255,255,0.06)",
|
|
395
|
+
display: "flex",
|
|
396
|
+
alignItems: "center",
|
|
397
|
+
justifyContent: "center",
|
|
398
|
+
fontSize: "1rem"
|
|
399
|
+
}
|
|
400
|
+
}, PLATFORM_ICONS[p.platform] || "\u{1F517}"),
|
|
401
|
+
// Info
|
|
210
402
|
h(
|
|
211
403
|
"div",
|
|
212
|
-
|
|
213
|
-
h("div", { className: "text-sm font-medium", style: t.text },
|
|
214
|
-
h(
|
|
404
|
+
{ style: { flex: 1, minWidth: 0 } },
|
|
405
|
+
h("div", { className: "text-sm font-medium", style: t.text }, p.displayName),
|
|
406
|
+
h(
|
|
407
|
+
"div",
|
|
408
|
+
{ className: "text-xs", style: t.faint },
|
|
409
|
+
[p.platform, p.username].filter(Boolean).join(" \xB7 ")
|
|
410
|
+
)
|
|
215
411
|
),
|
|
216
|
-
|
|
412
|
+
// Status + capabilities
|
|
413
|
+
h(
|
|
414
|
+
"div",
|
|
415
|
+
{ className: "flex items-center gap-2 shrink-0" },
|
|
416
|
+
p.capabilities?.includes("schedule") && h("span", { className: "text-xs", style: t.faint }, "\u23F1"),
|
|
417
|
+
p.capabilities?.includes("post") && h("span", { className: "text-xs", style: t.faint }, "\u{1F4E4}"),
|
|
418
|
+
h("div", {
|
|
419
|
+
style: {
|
|
420
|
+
width: 8,
|
|
421
|
+
height: 8,
|
|
422
|
+
borderRadius: "50%",
|
|
423
|
+
background: p.isActive ? "rgba(74,222,128,0.8)" : "rgba(248,113,113,0.6)"
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
)
|
|
217
427
|
)
|
|
218
428
|
)
|
|
219
429
|
)
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
const useEffect = R.useEffect;
|
|
8
8
|
const useMemo = R.useMemo;
|
|
9
9
|
const useState = R.useState;
|
|
10
|
+
const useCallback = R.useCallback;
|
|
10
11
|
const t = {
|
|
11
12
|
text: { color: "var(--ck-text-primary)" },
|
|
12
13
|
muted: { color: "var(--ck-text-secondary)" },
|
|
@@ -43,6 +44,15 @@
|
|
|
43
44
|
fontWeight: 600,
|
|
44
45
|
cursor: "pointer"
|
|
45
46
|
},
|
|
47
|
+
btnPublish: {
|
|
48
|
+
background: "rgba(99,179,237,0.2)",
|
|
49
|
+
border: "1px solid rgba(99,179,237,0.4)",
|
|
50
|
+
borderRadius: "10px",
|
|
51
|
+
padding: "0.6rem 0.85rem",
|
|
52
|
+
color: "rgba(210,235,255,0.95)",
|
|
53
|
+
fontWeight: 700,
|
|
54
|
+
cursor: "pointer"
|
|
55
|
+
},
|
|
46
56
|
pill: (active) => ({
|
|
47
57
|
background: active ? "rgba(99,179,237,0.16)" : "rgba(255,255,255,0.03)",
|
|
48
58
|
border: `1px solid ${active ? "rgba(99,179,237,0.45)" : "var(--ck-border-subtle)"}`,
|
|
@@ -52,81 +62,162 @@
|
|
|
52
62
|
color: active ? "rgba(210,235,255,0.95)" : "var(--ck-text-secondary)",
|
|
53
63
|
cursor: "pointer",
|
|
54
64
|
userSelect: "none"
|
|
55
|
-
})
|
|
65
|
+
}),
|
|
66
|
+
statusBadge: (status) => {
|
|
67
|
+
const colors = {
|
|
68
|
+
draft: "rgba(167,139,250,0.7)",
|
|
69
|
+
scheduled: "rgba(251,191,36,0.7)",
|
|
70
|
+
published: "rgba(74,222,128,0.7)",
|
|
71
|
+
failed: "rgba(248,113,113,0.7)"
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
display: "inline-block",
|
|
75
|
+
background: colors[status] || "rgba(100,100,100,0.5)",
|
|
76
|
+
borderRadius: "999px",
|
|
77
|
+
padding: "0.1rem 0.45rem",
|
|
78
|
+
fontSize: "0.7rem",
|
|
79
|
+
fontWeight: 600,
|
|
80
|
+
color: "white"
|
|
81
|
+
};
|
|
82
|
+
}
|
|
56
83
|
};
|
|
57
84
|
function ContentLibrary(props) {
|
|
58
85
|
const teamId = String(props?.teamId || "default");
|
|
86
|
+
const apiBase = useMemo(() => `/api/plugins/marketing`, []);
|
|
59
87
|
const [posts, setPosts] = useState([]);
|
|
88
|
+
const [providers, setProviders] = useState([]);
|
|
60
89
|
const [loading, setLoading] = useState(true);
|
|
61
90
|
const [saving, setSaving] = useState(false);
|
|
91
|
+
const [publishing, setPublishing] = useState(false);
|
|
62
92
|
const [error, setError] = useState(null);
|
|
93
|
+
const [success, setSuccess] = useState(null);
|
|
63
94
|
const [content, setContent] = useState("");
|
|
64
|
-
const [
|
|
95
|
+
const [selectedProviders, setSelectedProviders] = useState([]);
|
|
65
96
|
const [scheduledAt, setScheduledAt] = useState("");
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
97
|
+
const postizHeaders = useMemo(() => {
|
|
98
|
+
try {
|
|
99
|
+
const stored = localStorage.getItem(`ck-postiz-${teamId}`);
|
|
100
|
+
if (stored) {
|
|
101
|
+
const parsed = JSON.parse(stored);
|
|
102
|
+
if (parsed.apiKey) {
|
|
103
|
+
return {
|
|
104
|
+
"x-postiz-api-key": parsed.apiKey,
|
|
105
|
+
"x-postiz-base-url": parsed.baseUrl || "https://api.postiz.com/public/v1"
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
return {};
|
|
112
|
+
}, [teamId]);
|
|
113
|
+
const loadPosts = useCallback(async () => {
|
|
71
114
|
try {
|
|
72
|
-
const res = await fetch(`${apiBase}/posts?team=${encodeURIComponent(teamId)}&limit=25
|
|
115
|
+
const res = await fetch(`${apiBase}/posts?team=${encodeURIComponent(teamId)}&limit=25`);
|
|
73
116
|
const json = await res.json();
|
|
74
117
|
setPosts(Array.isArray(json.data) ? json.data : []);
|
|
75
|
-
} catch
|
|
76
|
-
setError(e?.message || "Failed to load posts");
|
|
77
|
-
} finally {
|
|
78
|
-
setLoading(false);
|
|
118
|
+
} catch {
|
|
79
119
|
}
|
|
80
|
-
};
|
|
120
|
+
}, [apiBase, teamId]);
|
|
121
|
+
const loadProviders = useCallback(async () => {
|
|
122
|
+
try {
|
|
123
|
+
const res = await fetch(`${apiBase}/providers?team=${encodeURIComponent(teamId)}`, { headers: postizHeaders });
|
|
124
|
+
const json = await res.json();
|
|
125
|
+
const detected = Array.isArray(json.providers) ? json.providers : [];
|
|
126
|
+
setProviders(detected);
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
}, [apiBase, teamId, postizHeaders]);
|
|
81
130
|
useEffect(() => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
131
|
+
setLoading(true);
|
|
132
|
+
Promise.all([loadPosts(), loadProviders()]).finally(() => setLoading(false));
|
|
133
|
+
}, [loadPosts, loadProviders]);
|
|
134
|
+
const toggleProvider = (id) => {
|
|
135
|
+
setSelectedProviders(
|
|
136
|
+
(prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
|
|
137
|
+
);
|
|
89
138
|
};
|
|
90
|
-
const
|
|
139
|
+
const onSaveDraft = async () => {
|
|
140
|
+
if (!content.trim()) return;
|
|
91
141
|
setSaving(true);
|
|
92
142
|
setError(null);
|
|
93
143
|
try {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
144
|
+
const platforms = selectedProviders.map((id) => providers.find((p) => p.id === id)?.platform).filter(Boolean);
|
|
145
|
+
const res = await fetch(`${apiBase}/posts?team=${encodeURIComponent(teamId)}`, {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: { "content-type": "application/json" },
|
|
148
|
+
body: JSON.stringify({
|
|
149
|
+
content,
|
|
150
|
+
platforms: platforms.length > 0 ? platforms : ["draft"],
|
|
151
|
+
status: scheduledAt ? "scheduled" : "draft",
|
|
152
|
+
scheduledAt: scheduledAt || void 0
|
|
153
|
+
})
|
|
154
|
+
});
|
|
155
|
+
if (!res.ok) throw new Error(`Save failed (${res.status})`);
|
|
156
|
+
setContent("");
|
|
157
|
+
setScheduledAt("");
|
|
158
|
+
setSelectedProviders([]);
|
|
159
|
+
await loadPosts();
|
|
160
|
+
} catch (e) {
|
|
161
|
+
setError(e?.message || "Failed to save");
|
|
162
|
+
} finally {
|
|
163
|
+
setSaving(false);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
const onPublish = async () => {
|
|
167
|
+
if (!content.trim() || selectedProviders.length === 0) return;
|
|
168
|
+
setPublishing(true);
|
|
169
|
+
setError(null);
|
|
170
|
+
setSuccess(null);
|
|
171
|
+
const postizProviders = selectedProviders.filter((id) => id.startsWith("postiz:"));
|
|
172
|
+
const gatewayProviders = selectedProviders.filter((id) => id.startsWith("gateway:"));
|
|
173
|
+
try {
|
|
174
|
+
if (postizProviders.length > 0) {
|
|
175
|
+
const integrationIds = postizProviders.map((id) => {
|
|
176
|
+
const prov = providers.find((p) => p.id === id);
|
|
177
|
+
return prov?.meta?.postizId;
|
|
178
|
+
}).filter(Boolean);
|
|
179
|
+
const res = await fetch(`${apiBase}/publish?team=${encodeURIComponent(teamId)}`, {
|
|
97
180
|
method: "POST",
|
|
98
|
-
headers: { "content-type": "application/json" },
|
|
181
|
+
headers: { "content-type": "application/json", ...postizHeaders },
|
|
99
182
|
body: JSON.stringify({
|
|
100
183
|
content,
|
|
101
|
-
|
|
102
|
-
status,
|
|
184
|
+
integrationIds,
|
|
103
185
|
scheduledAt: scheduledAt || void 0
|
|
104
186
|
})
|
|
187
|
+
});
|
|
188
|
+
if (!res.ok) {
|
|
189
|
+
const err = await res.json().catch(() => null);
|
|
190
|
+
throw new Error(err?.message || `Postiz publish failed (${res.status})`);
|
|
105
191
|
}
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
192
|
+
}
|
|
193
|
+
if (gatewayProviders.length > 0 && postizProviders.length === 0) {
|
|
194
|
+
setSuccess("Saved! Gateway posting requires OpenClaw agent \u2014 use the workflow or ask your assistant to post.");
|
|
195
|
+
} else {
|
|
196
|
+
setSuccess(scheduledAt ? "Scheduled via Postiz!" : "Published via Postiz!");
|
|
110
197
|
}
|
|
111
198
|
setContent("");
|
|
112
199
|
setScheduledAt("");
|
|
113
|
-
|
|
200
|
+
setSelectedProviders([]);
|
|
201
|
+
await loadPosts();
|
|
114
202
|
} catch (e) {
|
|
115
|
-
setError(e?.message || "
|
|
203
|
+
setError(e?.message || "Publish failed");
|
|
116
204
|
} finally {
|
|
117
|
-
|
|
205
|
+
setPublishing(false);
|
|
118
206
|
}
|
|
119
207
|
};
|
|
208
|
+
const postizAvailable = providers.some((p) => p.type === "postiz");
|
|
209
|
+
const hasSelection = selectedProviders.length > 0;
|
|
120
210
|
return h(
|
|
121
211
|
"div",
|
|
122
212
|
{ className: "space-y-3" },
|
|
213
|
+
// ---- Composer ----
|
|
123
214
|
h(
|
|
124
215
|
"div",
|
|
125
216
|
{ style: t.card },
|
|
126
|
-
h("div", { className: "text-sm font-medium mb-3", style: t.text }, "
|
|
217
|
+
h("div", { className: "text-sm font-medium mb-3", style: t.text }, "Compose"),
|
|
127
218
|
h(
|
|
128
219
|
"div",
|
|
129
|
-
{ className: "space-y-
|
|
220
|
+
{ className: "space-y-3" },
|
|
130
221
|
h("textarea", {
|
|
131
222
|
value: content,
|
|
132
223
|
onChange: (e) => setContent(e.target.value),
|
|
@@ -134,20 +225,36 @@
|
|
|
134
225
|
rows: 5,
|
|
135
226
|
style: { ...t.input, resize: "vertical", minHeight: "110px" }
|
|
136
227
|
}),
|
|
137
|
-
|
|
228
|
+
// Provider selector
|
|
229
|
+
providers.length > 0 && h(
|
|
138
230
|
"div",
|
|
139
|
-
|
|
140
|
-
h("div", { className: "text-xs font-medium", style: t.faint }, "
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
231
|
+
null,
|
|
232
|
+
h("div", { className: "text-xs font-medium mb-2", style: t.faint }, "Publish to"),
|
|
233
|
+
h(
|
|
234
|
+
"div",
|
|
235
|
+
{ className: "flex flex-wrap gap-2" },
|
|
236
|
+
...providers.filter((p) => p.isActive).map(
|
|
237
|
+
(p) => h(
|
|
238
|
+
"span",
|
|
239
|
+
{
|
|
240
|
+
key: p.id,
|
|
241
|
+
onClick: () => toggleProvider(p.id),
|
|
242
|
+
style: t.pill(selectedProviders.includes(p.id)),
|
|
243
|
+
role: "button",
|
|
244
|
+
tabIndex: 0
|
|
245
|
+
},
|
|
246
|
+
`${p.displayName}`
|
|
247
|
+
)
|
|
248
|
+
)
|
|
149
249
|
)
|
|
150
250
|
),
|
|
251
|
+
// No providers hint
|
|
252
|
+
providers.length === 0 && !loading && h(
|
|
253
|
+
"div",
|
|
254
|
+
{ className: "text-xs", style: t.faint },
|
|
255
|
+
"No publishing targets detected. Go to Accounts tab to connect Postiz or add accounts."
|
|
256
|
+
),
|
|
257
|
+
// Schedule
|
|
151
258
|
h(
|
|
152
259
|
"div",
|
|
153
260
|
{ className: "grid grid-cols-1 gap-2 sm:grid-cols-2" },
|
|
@@ -164,38 +271,50 @@
|
|
|
164
271
|
),
|
|
165
272
|
h(
|
|
166
273
|
"div",
|
|
167
|
-
|
|
168
|
-
h(
|
|
169
|
-
|
|
274
|
+
{ className: "flex items-end" },
|
|
275
|
+
h(
|
|
276
|
+
"div",
|
|
277
|
+
{ className: "text-xs", style: t.faint },
|
|
278
|
+
content.length > 0 ? `${content.length} chars` : ""
|
|
279
|
+
)
|
|
170
280
|
)
|
|
171
281
|
),
|
|
282
|
+
// Actions
|
|
172
283
|
h(
|
|
173
284
|
"div",
|
|
174
285
|
{ className: "flex flex-wrap gap-2 items-center" },
|
|
175
286
|
h("button", {
|
|
176
287
|
type: "button",
|
|
177
|
-
onClick: () => void
|
|
178
|
-
style: t.btnGhost,
|
|
179
|
-
disabled: saving
|
|
180
|
-
}, loading ? "Refreshing\u2026" : "Refresh"),
|
|
181
|
-
h("button", {
|
|
182
|
-
type: "button",
|
|
183
|
-
onClick: () => void onCreate(),
|
|
184
|
-
style: { ...t.btnPrimary, opacity: saving ? 0.7 : 1 },
|
|
185
|
-
disabled: saving
|
|
288
|
+
onClick: () => void onSaveDraft(),
|
|
289
|
+
style: { ...t.btnGhost, opacity: saving ? 0.7 : 1 },
|
|
290
|
+
disabled: saving || !content.trim()
|
|
186
291
|
}, saving ? "Saving\u2026" : "Save draft"),
|
|
187
|
-
h("
|
|
292
|
+
postizAvailable && hasSelection && h("button", {
|
|
293
|
+
type: "button",
|
|
294
|
+
onClick: () => void onPublish(),
|
|
295
|
+
style: { ...t.btnPublish, opacity: publishing ? 0.7 : 1 },
|
|
296
|
+
disabled: publishing || !content.trim()
|
|
297
|
+
}, publishing ? "Publishing\u2026" : scheduledAt ? "\u23F1 Schedule" : "\u{1F4E4} Publish"),
|
|
298
|
+
!postizAvailable && hasSelection && h(
|
|
299
|
+
"div",
|
|
300
|
+
{ className: "text-xs", style: t.faint },
|
|
301
|
+
"Connect Postiz on Accounts tab to publish directly."
|
|
302
|
+
)
|
|
188
303
|
),
|
|
189
|
-
error
|
|
190
|
-
|
|
191
|
-
style: { color: "rgba(248,113,113,0.95)" }
|
|
192
|
-
}, error) : null
|
|
304
|
+
error && h("div", { className: "text-xs", style: { color: "rgba(248,113,113,0.95)" } }, error),
|
|
305
|
+
success && h("div", { className: "text-xs", style: { color: "rgba(74,222,128,0.9)" } }, success)
|
|
193
306
|
)
|
|
194
307
|
),
|
|
308
|
+
// ---- Posts list ----
|
|
195
309
|
h(
|
|
196
310
|
"div",
|
|
197
311
|
{ style: t.card },
|
|
198
|
-
h(
|
|
312
|
+
h(
|
|
313
|
+
"div",
|
|
314
|
+
{ className: "flex items-center justify-between mb-2" },
|
|
315
|
+
h("div", { className: "text-sm font-medium", style: t.text }, "Posts"),
|
|
316
|
+
h("button", { type: "button", onClick: () => void loadPosts(), style: t.btnGhost, className: "text-xs" }, "\u21BB")
|
|
317
|
+
),
|
|
199
318
|
loading ? h("div", { className: "py-6 text-center text-sm", style: t.faint }, "Loading\u2026") : posts.length === 0 ? h("div", { className: "py-6 text-center text-sm", style: t.faint }, "No posts yet.") : h(
|
|
200
319
|
"div",
|
|
201
320
|
{ className: "space-y-2" },
|
|
@@ -206,14 +325,19 @@
|
|
|
206
325
|
h(
|
|
207
326
|
"div",
|
|
208
327
|
{ className: "flex items-center justify-between gap-2" },
|
|
209
|
-
h(
|
|
210
|
-
|
|
328
|
+
h(
|
|
329
|
+
"div",
|
|
330
|
+
{ className: "flex items-center gap-2" },
|
|
331
|
+
h("span", { style: t.statusBadge(p.status) }, p.status),
|
|
332
|
+
h("span", { className: "text-xs", style: t.faint }, new Date(p.createdAt).toLocaleString())
|
|
333
|
+
),
|
|
334
|
+
p.scheduledAt && h("div", { className: "text-xs", style: t.muted }, `\u23F1 ${new Date(p.scheduledAt).toLocaleString()}`)
|
|
211
335
|
),
|
|
212
336
|
h("div", { className: "mt-2 whitespace-pre-wrap text-sm", style: t.text }, p.content),
|
|
213
|
-
h(
|
|
337
|
+
p.platforms?.length > 0 && h(
|
|
214
338
|
"div",
|
|
215
|
-
{ className: "mt-2 flex flex-wrap gap-
|
|
216
|
-
...
|
|
339
|
+
{ className: "mt-2 flex flex-wrap gap-1" },
|
|
340
|
+
...p.platforms.map((pl) => h("span", { key: pl, style: t.pill(true) }, pl))
|
|
217
341
|
)
|
|
218
342
|
)
|
|
219
343
|
)
|