@jiggai/kitchen-plugin-marketing 0.2.9 → 0.2.11

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.
@@ -53,59 +53,59 @@
53
53
  fontSize: "0.7rem",
54
54
  fontWeight: 600,
55
55
  color: "white"
56
+ }),
57
+ capPill: (active) => ({
58
+ display: "inline-block",
59
+ background: active ? "rgba(99,179,237,0.15)" : "rgba(255,255,255,0.03)",
60
+ border: `1px solid ${active ? "rgba(99,179,237,0.3)" : "var(--ck-border-subtle)"}`,
61
+ borderRadius: "999px",
62
+ padding: "0.1rem 0.4rem",
63
+ fontSize: "0.6rem",
64
+ color: active ? "rgba(210,235,255,0.9)" : "var(--ck-text-tertiary)"
56
65
  })
57
66
  };
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 = {
67
+ const BACKEND_COLORS = {
77
68
  postiz: "rgba(99,179,237,0.7)",
78
69
  gateway: "rgba(134,239,172,0.7)",
79
- skill: "rgba(251,191,36,0.7)",
80
- manual: "rgba(167,139,250,0.7)"
70
+ direct: "rgba(251,191,36,0.7)",
71
+ none: "rgba(100,100,100,0.5)"
72
+ };
73
+ const BACKEND_LABELS = {
74
+ postiz: "Postiz",
75
+ gateway: "OpenClaw",
76
+ direct: "Direct API",
77
+ none: "Not connected"
81
78
  };
82
79
  function Accounts(props) {
83
80
  const teamId = String(props?.teamId || "default");
84
81
  const apiBase = useMemo(() => `/api/plugins/marketing`, []);
85
- const [providers, setProviders] = useState([]);
82
+ const [drivers, setDrivers] = useState([]);
86
83
  const [manualAccounts, setManualAccounts] = useState([]);
87
84
  const [loading, setLoading] = useState(true);
88
- const [detecting, setDetecting] = useState(false);
89
85
  const [error, setError] = useState(null);
90
86
  const [postizKey, setPostizKey] = useState("");
91
87
  const [postizUrl, setPostizUrl] = useState("https://api.postiz.com/public/v1");
92
88
  const [showPostizSetup, setShowPostizSetup] = useState(false);
93
89
  const [showManual, setShowManual] = useState(false);
94
- const [manPlatform, setManPlatform] = useState("twitter");
90
+ const [manPlatform, setManPlatform] = useState("x");
95
91
  const [manName, setManName] = useState("");
96
92
  const [manUser, setManUser] = useState("");
97
93
  const [manToken, setManToken] = useState("");
98
94
  const [saving, setSaving] = useState(false);
99
- useEffect(() => {
95
+ const getStoredPostiz = () => {
100
96
  try {
101
97
  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
- }
98
+ if (stored) return JSON.parse(stored);
107
99
  } catch {
108
100
  }
101
+ return null;
102
+ };
103
+ useEffect(() => {
104
+ const stored = getStoredPostiz();
105
+ if (stored) {
106
+ setPostizKey(stored.apiKey || "");
107
+ setPostizUrl(stored.baseUrl || "https://api.postiz.com/public/v1");
108
+ }
109
109
  }, [teamId]);
110
110
  const savePostizConfig = () => {
111
111
  try {
@@ -113,18 +113,9 @@
113
113
  } catch {
114
114
  }
115
115
  setShowPostizSetup(false);
116
- void detectAll();
116
+ void loadDrivers();
117
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);
118
+ const loadDrivers = async () => {
128
119
  setError(null);
129
120
  try {
130
121
  const stored = getStoredPostiz();
@@ -135,13 +126,20 @@
135
126
  headers["x-postiz-api-key"] = key;
136
127
  headers["x-postiz-base-url"] = url;
137
128
  }
138
- const res = await fetch(`${apiBase}/providers?team=${encodeURIComponent(teamId)}`, { headers });
129
+ const res = await fetch(`${apiBase}/drivers?team=${encodeURIComponent(teamId)}`, { headers });
130
+ if (!res.ok) {
131
+ const errText = await res.text().catch(() => `HTTP ${res.status}`);
132
+ setError(`Driver API error: ${res.status} \u2014 ${errText.slice(0, 200)}`);
133
+ return;
134
+ }
139
135
  const json = await res.json();
140
- setProviders(Array.isArray(json.providers) ? json.providers : []);
136
+ const list = Array.isArray(json.drivers) ? json.drivers : json.data?.drivers || [];
137
+ setDrivers(list);
138
+ if (list.length === 0) {
139
+ setError("No platform drivers returned. The plugin handler may not be loaded \u2014 try restarting Kitchen.");
140
+ }
141
141
  } catch (e) {
142
- setError(e?.message || "Failed to detect providers");
143
- } finally {
144
- setDetecting(false);
142
+ setError(`Failed to load drivers: ${e?.message || "unknown"}. Check browser console for details.`);
145
143
  }
146
144
  };
147
145
  const loadManual = async () => {
@@ -154,7 +152,7 @@
154
152
  };
155
153
  const refresh = async () => {
156
154
  setLoading(true);
157
- await Promise.all([detectAll(), loadManual()]);
155
+ await Promise.all([loadDrivers(), loadManual()]);
158
156
  setLoading(false);
159
157
  };
160
158
  useEffect(() => {
@@ -179,43 +177,17 @@
179
177
  setManName("");
180
178
  setManUser("");
181
179
  setManToken("");
182
- await loadManual();
180
+ await Promise.all([loadDrivers(), loadManual()]);
183
181
  } catch (e) {
184
182
  setError(e?.message || "Failed to connect");
185
183
  } finally {
186
184
  setSaving(false);
187
185
  }
188
186
  };
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
- };
187
+ const connectedDrivers = useMemo(() => drivers.filter((d) => d.connected), [drivers]);
188
+ const disconnectedDrivers = useMemo(() => drivers.filter((d) => !d.connected), [drivers]);
189
+ const connectedCount = connectedDrivers.length;
190
+ const totalCount = drivers.length;
219
191
  return h(
220
192
  "div",
221
193
  { className: "space-y-3" },
@@ -229,11 +201,11 @@
229
201
  h(
230
202
  "div",
231
203
  null,
232
- h("div", { className: "text-sm font-medium", style: t.text }, "Connected Accounts"),
204
+ h("div", { className: "text-sm font-medium", style: t.text }, "Platform Drivers"),
233
205
  h(
234
206
  "div",
235
207
  { className: "mt-1 text-xs", style: t.faint },
236
- `${allProviders.length} provider${allProviders.length !== 1 ? "s" : ""} detected`
208
+ `${connectedCount}/${totalCount} platforms connected`
237
209
  )
238
210
  ),
239
211
  h(
@@ -241,15 +213,15 @@
241
213
  { className: "flex flex-wrap gap-2" },
242
214
  h(
243
215
  "button",
244
- { type: "button", onClick: () => void refresh(), style: t.btnGhost, disabled: detecting },
245
- detecting ? "Detecting\u2026" : "\u21BB Refresh"
216
+ { type: "button", onClick: () => void refresh(), style: t.btnGhost, disabled: loading },
217
+ loading ? "Loading\u2026" : "\u21BB Refresh"
246
218
  ),
247
219
  h(
248
220
  "button",
249
221
  { type: "button", onClick: () => setShowPostizSetup(!showPostizSetup), style: t.btnGhost },
250
222
  postizKey ? "\u2699 Postiz" : "+ Postiz"
251
223
  ),
252
- h("button", { type: "button", onClick: () => setShowManual(!showManual), style: t.btnGhost }, "+ Manual")
224
+ h("button", { type: "button", onClick: () => setShowManual(!showManual), style: t.btnGhost }, "+ Direct token")
253
225
  )
254
226
  ),
255
227
  error && h("div", { className: "mt-2 text-xs", style: { color: "rgba(248,113,113,0.95)" } }, error)
@@ -262,7 +234,7 @@
262
234
  h(
263
235
  "div",
264
236
  { 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."
237
+ "Postiz manages OAuth connections to social platforms. Get your API key from Postiz Settings \u2192 Developers \u2192 Public API."
266
238
  ),
267
239
  h(
268
240
  "div",
@@ -295,15 +267,15 @@
295
267
  "div",
296
268
  { className: "mt-3 flex gap-2" },
297
269
  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")
270
+ h("button", { type: "button", onClick: savePostizConfig, style: t.btnPrimary }, "Save & Detect")
299
271
  )
300
272
  ),
301
- // ---- Manual account form ----
273
+ // ---- Manual token form ----
302
274
  showManual && h(
303
275
  "div",
304
276
  { style: t.card },
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."),
277
+ h("div", { className: "text-sm font-medium mb-2", style: t.text }, "Add direct API token"),
278
+ h("div", { className: "text-xs mb-3", style: t.faint }, "For platforms where you have your own API credentials. Token is encrypted at rest."),
307
279
  h(
308
280
  "div",
309
281
  { className: "grid grid-cols-1 gap-2 sm:grid-cols-2" },
@@ -314,18 +286,14 @@
314
286
  h(
315
287
  "select",
316
288
  { value: manPlatform, onChange: (e) => setManPlatform(e.target.value), style: t.input },
317
- h("option", { value: "twitter" }, "Twitter / X"),
318
- h("option", { value: "instagram" }, "Instagram"),
319
- h("option", { value: "linkedin" }, "LinkedIn"),
320
- h("option", { value: "bluesky" }, "Bluesky"),
321
- h("option", { value: "mastodon" }, "Mastodon")
289
+ ...drivers.map((d) => h("option", { key: d.platform, value: d.platform }, d.label))
322
290
  )
323
291
  ),
324
292
  h(
325
293
  "div",
326
294
  null,
327
295
  h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "Display name"),
328
- h("input", { value: manName, onChange: (e) => setManName(e.target.value), placeholder: "My X account", style: t.input })
296
+ h("input", { value: manName, onChange: (e) => setManName(e.target.value), placeholder: "My account", style: t.input })
329
297
  ),
330
298
  h(
331
299
  "div",
@@ -347,87 +315,168 @@
347
315
  h("button", { type: "button", onClick: () => void onManualConnect(), style: t.btnPrimary, disabled: saving }, saving ? "Saving\u2026" : "Connect")
348
316
  )
349
317
  ),
350
- // ---- Loading ----
351
- loading && h(
352
- "div",
353
- { style: t.card },
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(
318
+ // ---- Connected platforms ----
319
+ connectedDrivers.length > 0 && h(
358
320
  "div",
359
321
  { style: t.card },
360
322
  h(
361
323
  "div",
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."
324
+ { className: "flex items-center gap-2 mb-3" },
325
+ h("div", { className: "text-sm font-medium", style: t.text }, "Connected"),
326
+ h("span", { style: t.badge("rgba(74,222,128,0.7)") }, `${connectedCount}`)
327
+ ),
328
+ h(
329
+ "div",
330
+ { className: "space-y-2" },
331
+ ...connectedDrivers.map(
332
+ (d) => h(
333
+ "div",
334
+ { key: d.platform, style: { ...t.card, padding: "0.75rem", display: "flex", alignItems: "center", gap: "0.75rem" } },
335
+ d.avatar ? h("img", { src: d.avatar, alt: "", style: { width: 36, height: 36, borderRadius: "50%", objectFit: "cover" } }) : h("div", {
336
+ style: {
337
+ width: 36,
338
+ height: 36,
339
+ borderRadius: "50%",
340
+ background: "rgba(255,255,255,0.06)",
341
+ display: "flex",
342
+ alignItems: "center",
343
+ justifyContent: "center",
344
+ fontSize: "1.1rem"
345
+ }
346
+ }, d.icon),
347
+ h(
348
+ "div",
349
+ { style: { flex: 1, minWidth: 0 } },
350
+ h("div", { className: "text-sm font-medium", style: t.text }, d.displayName),
351
+ h(
352
+ "div",
353
+ { className: "text-xs", style: t.faint },
354
+ [d.username, d.platform].filter(Boolean).join(" \xB7 ")
355
+ )
356
+ ),
357
+ h(
358
+ "div",
359
+ { className: "flex items-center gap-2 shrink-0 flex-wrap" },
360
+ h("span", { style: t.badge(BACKEND_COLORS[d.backend] || BACKEND_COLORS.none) }, BACKEND_LABELS[d.backend] || d.backend),
361
+ d.capabilities.canPost && h("span", { style: t.capPill(true) }, "post"),
362
+ d.capabilities.canSchedule && h("span", { style: t.capPill(true) }, "schedule"),
363
+ d.capabilities.canUploadMedia && h("span", { style: t.capPill(true) }, "media"),
364
+ d.capabilities.maxLength && h("span", { style: t.capPill(false) }, `${d.capabilities.maxLength} chars`),
365
+ h("div", {
366
+ style: {
367
+ width: 8,
368
+ height: 8,
369
+ borderRadius: "50%",
370
+ background: "rgba(74,222,128,0.8)"
371
+ }
372
+ })
373
+ )
374
+ )
368
375
  )
369
376
  )
370
377
  ),
371
- !loading && Object.entries(grouped).map(
372
- ([type, items]) => h(
378
+ // ---- Disconnected platforms ----
379
+ disconnectedDrivers.length > 0 && h(
380
+ "div",
381
+ { style: t.card },
382
+ h(
373
383
  "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(
384
+ { className: "flex items-center gap-2 mb-3" },
385
+ h("div", { className: "text-sm font-medium", style: t.text }, "Available"),
386
+ h("span", { style: t.badge("rgba(100,100,100,0.5)") }, `${disconnectedDrivers.length}`)
387
+ ),
388
+ h(
389
+ "div",
390
+ { className: "text-xs mb-3", style: t.faint },
391
+ "Connect these via Postiz or by adding a direct API token above."
392
+ ),
393
+ h(
394
+ "div",
395
+ { className: "space-y-2" },
396
+ ...disconnectedDrivers.map(
397
+ (d) => h(
398
+ "div",
399
+ { key: d.platform, style: { ...t.card, padding: "0.75rem", display: "flex", alignItems: "center", gap: "0.75rem", opacity: 0.6 } },
400
+ h("div", {
401
+ style: {
402
+ width: 36,
403
+ height: 36,
404
+ borderRadius: "50%",
405
+ background: "rgba(255,255,255,0.04)",
406
+ display: "flex",
407
+ alignItems: "center",
408
+ justifyContent: "center",
409
+ fontSize: "1.1rem"
410
+ }
411
+ }, d.icon),
412
+ h(
413
+ "div",
414
+ { style: { flex: 1, minWidth: 0 } },
415
+ h("div", { className: "text-sm font-medium", style: t.text }, d.label),
416
+ h("div", { className: "text-xs", style: t.faint }, "Not connected")
417
+ ),
418
+ h(
386
419
  "div",
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", {
420
+ { className: "flex items-center gap-2 shrink-0" },
421
+ d.capabilities.maxLength && h("span", { style: t.capPill(false) }, `${d.capabilities.maxLength} chars`),
422
+ h("div", {
390
423
  style: {
391
- width: 32,
392
- height: 32,
424
+ width: 8,
425
+ height: 8,
393
426
  borderRadius: "50%",
394
- background: "rgba(255,255,255,0.06)",
395
- display: "flex",
396
- alignItems: "center",
397
- justifyContent: "center",
398
- fontSize: "1rem"
427
+ background: "rgba(100,100,100,0.4)"
399
428
  }
400
- }, PLATFORM_ICONS[p.platform] || "\u{1F517}"),
401
- // Info
402
- h(
403
- "div",
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
- )
411
- ),
412
- // Status + capabilities
429
+ })
430
+ )
431
+ )
432
+ )
433
+ )
434
+ ),
435
+ // ---- Manual accounts (if any exist beyond drivers) ----
436
+ manualAccounts.length > 0 && h(
437
+ "div",
438
+ { style: t.card },
439
+ h(
440
+ "div",
441
+ { className: "flex items-center gap-2 mb-3" },
442
+ h("div", { className: "text-sm font-medium", style: t.text }, "Stored tokens"),
443
+ h("span", { style: t.badge("rgba(251,191,36,0.7)") }, `${manualAccounts.length}`)
444
+ ),
445
+ h("div", { className: "text-xs mb-3", style: t.faint }, "Tokens stored locally, encrypted at rest. These feed into the direct backend for their platform driver."),
446
+ h(
447
+ "div",
448
+ { className: "space-y-2" },
449
+ ...manualAccounts.map(
450
+ (a) => h(
451
+ "div",
452
+ { key: a.id, style: { ...t.card, padding: "0.75rem", display: "flex", alignItems: "center", gap: "0.75rem" } },
453
+ h(
454
+ "div",
455
+ { style: { flex: 1, minWidth: 0 } },
456
+ h("div", { className: "text-sm", style: t.text }, a.displayName),
413
457
  h(
414
458
  "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
- })
459
+ { className: "text-xs", style: t.faint },
460
+ [a.platform, a.username].filter(Boolean).join(" \xB7 ")
426
461
  )
427
- )
462
+ ),
463
+ h("div", {
464
+ style: {
465
+ width: 8,
466
+ height: 8,
467
+ borderRadius: "50%",
468
+ background: a.isActive ? "rgba(74,222,128,0.8)" : "rgba(248,113,113,0.6)"
469
+ }
470
+ })
428
471
  )
429
472
  )
430
473
  )
474
+ ),
475
+ // ---- Loading ----
476
+ loading && drivers.length === 0 && h(
477
+ "div",
478
+ { style: t.card },
479
+ h("div", { className: "py-6 text-center text-sm", style: t.faint }, "Detecting platform drivers\u2026")
431
480
  )
432
481
  );
433
482
  }