@jiggai/kitchen-plugin-marketing 0.2.6 → 0.2.7
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 +293 -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,186 @@
|
|
|
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 detectAll = async () => {
|
|
119
|
+
setDetecting(true);
|
|
61
120
|
setError(null);
|
|
62
121
|
try {
|
|
63
|
-
const
|
|
122
|
+
const headers = {};
|
|
123
|
+
if (postizKey) {
|
|
124
|
+
headers["x-postiz-api-key"] = postizKey;
|
|
125
|
+
headers["x-postiz-base-url"] = postizUrl;
|
|
126
|
+
}
|
|
127
|
+
const res = await fetch(`${apiBase}/providers?team=${encodeURIComponent(teamId)}`, { headers });
|
|
64
128
|
const json = await res.json();
|
|
65
|
-
|
|
129
|
+
setProviders(Array.isArray(json.providers) ? json.providers : []);
|
|
66
130
|
} catch (e) {
|
|
67
|
-
setError(e?.message || "Failed to
|
|
131
|
+
setError(e?.message || "Failed to detect providers");
|
|
68
132
|
} finally {
|
|
69
|
-
|
|
133
|
+
setDetecting(false);
|
|
70
134
|
}
|
|
71
135
|
};
|
|
136
|
+
const loadManual = async () => {
|
|
137
|
+
try {
|
|
138
|
+
const res = await fetch(`${apiBase}/accounts?team=${encodeURIComponent(teamId)}`);
|
|
139
|
+
const json = await res.json();
|
|
140
|
+
setManualAccounts(Array.isArray(json.accounts) ? json.accounts : []);
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const refresh = async () => {
|
|
145
|
+
setLoading(true);
|
|
146
|
+
await Promise.all([detectAll(), loadManual()]);
|
|
147
|
+
setLoading(false);
|
|
148
|
+
};
|
|
72
149
|
useEffect(() => {
|
|
73
150
|
void refresh();
|
|
74
151
|
}, [teamId]);
|
|
75
|
-
const
|
|
152
|
+
const onManualConnect = async () => {
|
|
76
153
|
setSaving(true);
|
|
77
154
|
setError(null);
|
|
78
155
|
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();
|
|
156
|
+
const res = await fetch(`${apiBase}/accounts?team=${encodeURIComponent(teamId)}`, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: { "content-type": "application/json" },
|
|
159
|
+
body: JSON.stringify({
|
|
160
|
+
platform: manPlatform,
|
|
161
|
+
displayName: manName || `${manPlatform} account`,
|
|
162
|
+
username: manUser || void 0,
|
|
163
|
+
credentials: { accessToken: manToken }
|
|
164
|
+
})
|
|
165
|
+
});
|
|
166
|
+
if (!res.ok) throw new Error(`Failed (${res.status})`);
|
|
167
|
+
setShowManual(false);
|
|
168
|
+
setManName("");
|
|
169
|
+
setManUser("");
|
|
170
|
+
setManToken("");
|
|
171
|
+
await loadManual();
|
|
102
172
|
} catch (e) {
|
|
103
|
-
setError(e?.message || "Failed to connect
|
|
173
|
+
setError(e?.message || "Failed to connect");
|
|
104
174
|
} finally {
|
|
105
175
|
setSaving(false);
|
|
106
176
|
}
|
|
107
177
|
};
|
|
178
|
+
const allProviders = useMemo(() => {
|
|
179
|
+
const combined = [...providers];
|
|
180
|
+
for (const ma of manualAccounts) {
|
|
181
|
+
combined.push({
|
|
182
|
+
id: `manual:${ma.id}`,
|
|
183
|
+
type: "manual",
|
|
184
|
+
platform: ma.platform,
|
|
185
|
+
displayName: ma.displayName,
|
|
186
|
+
username: ma.username,
|
|
187
|
+
isActive: ma.isActive,
|
|
188
|
+
capabilities: ["post"]
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return combined;
|
|
192
|
+
}, [providers, manualAccounts]);
|
|
193
|
+
const grouped = useMemo(() => {
|
|
194
|
+
const g = {};
|
|
195
|
+
for (const p of allProviders) {
|
|
196
|
+
const key = p.type;
|
|
197
|
+
if (!g[key]) g[key] = [];
|
|
198
|
+
g[key].push(p);
|
|
199
|
+
}
|
|
200
|
+
return g;
|
|
201
|
+
}, [allProviders]);
|
|
202
|
+
const typeLabels = {
|
|
203
|
+
postiz: "Postiz",
|
|
204
|
+
gateway: "OpenClaw Channels",
|
|
205
|
+
skill: "Skills",
|
|
206
|
+
manual: "Manual"
|
|
207
|
+
};
|
|
108
208
|
return h(
|
|
109
209
|
"div",
|
|
110
210
|
{ className: "space-y-3" },
|
|
211
|
+
// ---- Header ----
|
|
111
212
|
h(
|
|
112
213
|
"div",
|
|
113
214
|
{ style: t.card },
|
|
@@ -117,22 +218,81 @@
|
|
|
117
218
|
h(
|
|
118
219
|
"div",
|
|
119
220
|
null,
|
|
120
|
-
h("div", { className: "text-sm font-medium", style: t.text }, "Accounts"),
|
|
121
|
-
h(
|
|
221
|
+
h("div", { className: "text-sm font-medium", style: t.text }, "Connected Accounts"),
|
|
222
|
+
h(
|
|
223
|
+
"div",
|
|
224
|
+
{ className: "mt-1 text-xs", style: t.faint },
|
|
225
|
+
`${allProviders.length} provider${allProviders.length !== 1 ? "s" : ""} detected`
|
|
226
|
+
)
|
|
122
227
|
),
|
|
123
228
|
h(
|
|
124
229
|
"div",
|
|
125
|
-
{ className: "flex gap-2" },
|
|
126
|
-
h(
|
|
127
|
-
|
|
230
|
+
{ className: "flex flex-wrap gap-2" },
|
|
231
|
+
h(
|
|
232
|
+
"button",
|
|
233
|
+
{ type: "button", onClick: () => void refresh(), style: t.btnGhost, disabled: detecting },
|
|
234
|
+
detecting ? "Detecting\u2026" : "\u21BB Refresh"
|
|
235
|
+
),
|
|
236
|
+
h(
|
|
237
|
+
"button",
|
|
238
|
+
{ type: "button", onClick: () => setShowPostizSetup(!showPostizSetup), style: t.btnGhost },
|
|
239
|
+
postizKey ? "\u2699 Postiz" : "+ Postiz"
|
|
240
|
+
),
|
|
241
|
+
h("button", { type: "button", onClick: () => setShowManual(!showManual), style: t.btnGhost }, "+ Manual")
|
|
128
242
|
)
|
|
129
243
|
),
|
|
130
|
-
error
|
|
244
|
+
error && h("div", { className: "mt-2 text-xs", style: { color: "rgba(248,113,113,0.95)" } }, error)
|
|
131
245
|
),
|
|
132
|
-
|
|
246
|
+
// ---- Postiz setup ----
|
|
247
|
+
showPostizSetup && h(
|
|
133
248
|
"div",
|
|
134
249
|
{ style: t.card },
|
|
135
|
-
h("div", { className: "text-sm font-medium mb-
|
|
250
|
+
h("div", { className: "text-sm font-medium mb-2", style: t.text }, "Postiz Configuration"),
|
|
251
|
+
h(
|
|
252
|
+
"div",
|
|
253
|
+
{ className: "text-xs mb-3", style: t.faint },
|
|
254
|
+
"Connect Postiz to manage social accounts via their platform. Get your API key from Postiz Settings \u2192 Developers \u2192 Public API."
|
|
255
|
+
),
|
|
256
|
+
h(
|
|
257
|
+
"div",
|
|
258
|
+
{ className: "grid grid-cols-1 gap-2 sm:grid-cols-2" },
|
|
259
|
+
h(
|
|
260
|
+
"div",
|
|
261
|
+
null,
|
|
262
|
+
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "API Key"),
|
|
263
|
+
h("input", {
|
|
264
|
+
type: "password",
|
|
265
|
+
value: postizKey,
|
|
266
|
+
onChange: (e) => setPostizKey(e.target.value),
|
|
267
|
+
placeholder: "your-postiz-api-key",
|
|
268
|
+
style: t.input
|
|
269
|
+
})
|
|
270
|
+
),
|
|
271
|
+
h(
|
|
272
|
+
"div",
|
|
273
|
+
null,
|
|
274
|
+
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "Base URL"),
|
|
275
|
+
h("input", {
|
|
276
|
+
value: postizUrl,
|
|
277
|
+
onChange: (e) => setPostizUrl(e.target.value),
|
|
278
|
+
placeholder: "https://api.postiz.com/public/v1",
|
|
279
|
+
style: t.input
|
|
280
|
+
})
|
|
281
|
+
)
|
|
282
|
+
),
|
|
283
|
+
h(
|
|
284
|
+
"div",
|
|
285
|
+
{ className: "mt-3 flex gap-2" },
|
|
286
|
+
h("button", { type: "button", onClick: () => setShowPostizSetup(false), style: t.btnGhost }, "Cancel"),
|
|
287
|
+
h("button", { type: "button", onClick: savePostizConfig, style: t.btnPrimary }, postizKey ? "Save & Detect" : "Save")
|
|
288
|
+
)
|
|
289
|
+
),
|
|
290
|
+
// ---- Manual account form ----
|
|
291
|
+
showManual && h(
|
|
292
|
+
"div",
|
|
293
|
+
{ style: t.card },
|
|
294
|
+
h("div", { className: "text-sm font-medium mb-2", style: t.text }, "Add manual account"),
|
|
295
|
+
h("div", { className: "text-xs mb-3", style: t.faint }, "For direct API access without Postiz. You provide the token."),
|
|
136
296
|
h(
|
|
137
297
|
"div",
|
|
138
298
|
{ className: "grid grid-cols-1 gap-2 sm:grid-cols-2" },
|
|
@@ -142,78 +302,117 @@
|
|
|
142
302
|
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "Platform"),
|
|
143
303
|
h(
|
|
144
304
|
"select",
|
|
145
|
-
{
|
|
146
|
-
value: platform,
|
|
147
|
-
onChange: (e) => setPlatform(e.target.value),
|
|
148
|
-
style: t.input
|
|
149
|
-
},
|
|
305
|
+
{ value: manPlatform, onChange: (e) => setManPlatform(e.target.value), style: t.input },
|
|
150
306
|
h("option", { value: "twitter" }, "Twitter / X"),
|
|
151
307
|
h("option", { value: "instagram" }, "Instagram"),
|
|
152
|
-
h("option", { value: "linkedin" }, "LinkedIn")
|
|
308
|
+
h("option", { value: "linkedin" }, "LinkedIn"),
|
|
309
|
+
h("option", { value: "bluesky" }, "Bluesky"),
|
|
310
|
+
h("option", { value: "mastodon" }, "Mastodon")
|
|
153
311
|
)
|
|
154
312
|
),
|
|
155
313
|
h(
|
|
156
314
|
"div",
|
|
157
315
|
null,
|
|
158
316
|
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
|
-
})
|
|
317
|
+
h("input", { value: manName, onChange: (e) => setManName(e.target.value), placeholder: "My X account", style: t.input })
|
|
165
318
|
),
|
|
166
319
|
h(
|
|
167
320
|
"div",
|
|
168
321
|
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
|
-
})
|
|
322
|
+
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "Username"),
|
|
323
|
+
h("input", { value: manUser, onChange: (e) => setManUser(e.target.value), placeholder: "@handle", style: t.input })
|
|
176
324
|
),
|
|
177
325
|
h(
|
|
178
326
|
"div",
|
|
179
327
|
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
|
-
})
|
|
328
|
+
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "Access token"),
|
|
329
|
+
h("input", { type: "password", value: manToken, onChange: (e) => setManToken(e.target.value), placeholder: "token\u2026", style: t.input })
|
|
187
330
|
)
|
|
188
331
|
),
|
|
189
332
|
h(
|
|
190
333
|
"div",
|
|
191
334
|
{ className: "mt-3 flex gap-2" },
|
|
192
|
-
h("button", { type: "button", onClick: () =>
|
|
193
|
-
h("button", { type: "button", onClick: () => void
|
|
335
|
+
h("button", { type: "button", onClick: () => setShowManual(false), style: t.btnGhost }, "Cancel"),
|
|
336
|
+
h("button", { type: "button", onClick: () => void onManualConnect(), style: t.btnPrimary, disabled: saving }, saving ? "Saving\u2026" : "Connect")
|
|
194
337
|
)
|
|
195
|
-
)
|
|
196
|
-
|
|
338
|
+
),
|
|
339
|
+
// ---- Loading ----
|
|
340
|
+
loading && h(
|
|
341
|
+
"div",
|
|
342
|
+
{ style: t.card },
|
|
343
|
+
h("div", { className: "py-6 text-center text-sm", style: t.faint }, "Detecting providers\u2026")
|
|
344
|
+
),
|
|
345
|
+
// ---- Provider groups ----
|
|
346
|
+
!loading && allProviders.length === 0 && h(
|
|
197
347
|
"div",
|
|
198
348
|
{ style: t.card },
|
|
199
|
-
h(
|
|
200
|
-
loading ? h("div", { className: "py-6 text-center text-sm", style: t.faint }, "Loading\u2026") : accounts.length === 0 ? h("div", { className: "py-6 text-center text-sm", style: t.faint }, "No accounts connected yet.") : h(
|
|
349
|
+
h(
|
|
201
350
|
"div",
|
|
202
|
-
{ className: "space-y-2" },
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
351
|
+
{ className: "py-6 text-center space-y-2" },
|
|
352
|
+
h("div", { className: "text-sm", style: t.faint }, "No providers detected"),
|
|
353
|
+
h(
|
|
354
|
+
"div",
|
|
355
|
+
{ className: "text-xs", style: t.faint },
|
|
356
|
+
"Connect Postiz for full social media management, or add accounts manually."
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
),
|
|
360
|
+
!loading && Object.entries(grouped).map(
|
|
361
|
+
([type, items]) => h(
|
|
362
|
+
"div",
|
|
363
|
+
{ key: type, style: t.card },
|
|
364
|
+
h(
|
|
365
|
+
"div",
|
|
366
|
+
{ className: "flex items-center gap-2 mb-3" },
|
|
367
|
+
h("div", { className: "text-sm font-medium", style: t.text }, typeLabels[type] || type),
|
|
368
|
+
h("span", { style: t.badge(TYPE_COLORS[type] || "rgba(100,100,100,0.6)") }, `${items.length}`)
|
|
369
|
+
),
|
|
370
|
+
h(
|
|
371
|
+
"div",
|
|
372
|
+
{ className: "space-y-2" },
|
|
373
|
+
...items.map(
|
|
374
|
+
(p) => h(
|
|
208
375
|
"div",
|
|
209
|
-
{
|
|
376
|
+
{ key: p.id, style: { ...t.card, padding: "0.75rem", display: "flex", alignItems: "center", gap: "0.75rem" } },
|
|
377
|
+
// Avatar or platform icon
|
|
378
|
+
p.avatar ? h("img", { src: p.avatar, alt: "", style: { width: 32, height: 32, borderRadius: "50%", objectFit: "cover" } }) : h("div", {
|
|
379
|
+
style: {
|
|
380
|
+
width: 32,
|
|
381
|
+
height: 32,
|
|
382
|
+
borderRadius: "50%",
|
|
383
|
+
background: "rgba(255,255,255,0.06)",
|
|
384
|
+
display: "flex",
|
|
385
|
+
alignItems: "center",
|
|
386
|
+
justifyContent: "center",
|
|
387
|
+
fontSize: "1rem"
|
|
388
|
+
}
|
|
389
|
+
}, PLATFORM_ICONS[p.platform] || "\u{1F517}"),
|
|
390
|
+
// Info
|
|
210
391
|
h(
|
|
211
392
|
"div",
|
|
212
|
-
|
|
213
|
-
h("div", { className: "text-sm font-medium", style: t.text },
|
|
214
|
-
h(
|
|
393
|
+
{ style: { flex: 1, minWidth: 0 } },
|
|
394
|
+
h("div", { className: "text-sm font-medium", style: t.text }, p.displayName),
|
|
395
|
+
h(
|
|
396
|
+
"div",
|
|
397
|
+
{ className: "text-xs", style: t.faint },
|
|
398
|
+
[p.platform, p.username].filter(Boolean).join(" \xB7 ")
|
|
399
|
+
)
|
|
215
400
|
),
|
|
216
|
-
|
|
401
|
+
// Status + capabilities
|
|
402
|
+
h(
|
|
403
|
+
"div",
|
|
404
|
+
{ className: "flex items-center gap-2 shrink-0" },
|
|
405
|
+
p.capabilities?.includes("schedule") && h("span", { className: "text-xs", style: t.faint }, "\u23F1"),
|
|
406
|
+
p.capabilities?.includes("post") && h("span", { className: "text-xs", style: t.faint }, "\u{1F4E4}"),
|
|
407
|
+
h("div", {
|
|
408
|
+
style: {
|
|
409
|
+
width: 8,
|
|
410
|
+
height: 8,
|
|
411
|
+
borderRadius: "50%",
|
|
412
|
+
background: p.isActive ? "rgba(74,222,128,0.8)" : "rgba(248,113,113,0.6)"
|
|
413
|
+
}
|
|
414
|
+
})
|
|
415
|
+
)
|
|
217
416
|
)
|
|
218
417
|
)
|
|
219
418
|
)
|
|
@@ -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
|
)
|