@jiggai/kitchen-plugin-marketing 0.2.5 → 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 +510 -0
- package/dist/tabs/accounts.js +383 -22
- package/dist/tabs/content-library.js +315 -11
- package/package.json +2 -2
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc2) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc2 = __getOwnPropDesc(from, key)) || desc2.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// src/api/handler.ts
|
|
30
|
+
var handler_exports = {};
|
|
31
|
+
__export(handler_exports, {
|
|
32
|
+
handleRequest: () => handleRequest
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(handler_exports);
|
|
35
|
+
var import_drizzle_orm2 = require("drizzle-orm");
|
|
36
|
+
var import_crypto2 = require("crypto");
|
|
37
|
+
|
|
38
|
+
// src/db/index.ts
|
|
39
|
+
var import_better_sqlite3 = __toESM(require("better-sqlite3"));
|
|
40
|
+
var import_better_sqlite32 = require("drizzle-orm/better-sqlite3");
|
|
41
|
+
|
|
42
|
+
// src/db/schema.ts
|
|
43
|
+
var schema_exports = {};
|
|
44
|
+
__export(schema_exports, {
|
|
45
|
+
accountMetrics: () => accountMetrics,
|
|
46
|
+
accountMetricsRelations: () => accountMetricsRelations,
|
|
47
|
+
media: () => media,
|
|
48
|
+
postMetrics: () => postMetrics,
|
|
49
|
+
postMetricsRelations: () => postMetricsRelations,
|
|
50
|
+
posts: () => posts,
|
|
51
|
+
postsRelations: () => postsRelations,
|
|
52
|
+
socialAccounts: () => socialAccounts,
|
|
53
|
+
socialAccountsRelations: () => socialAccountsRelations,
|
|
54
|
+
templates: () => templates,
|
|
55
|
+
webhooks: () => webhooks
|
|
56
|
+
});
|
|
57
|
+
var import_sqlite_core = require("drizzle-orm/sqlite-core");
|
|
58
|
+
var import_drizzle_orm = require("drizzle-orm");
|
|
59
|
+
var posts = (0, import_sqlite_core.sqliteTable)("posts", {
|
|
60
|
+
id: (0, import_sqlite_core.text)("id").primaryKey(),
|
|
61
|
+
teamId: (0, import_sqlite_core.text)("team_id").notNull(),
|
|
62
|
+
content: (0, import_sqlite_core.text)("content").notNull(),
|
|
63
|
+
platforms: (0, import_sqlite_core.text)("platforms").notNull(),
|
|
64
|
+
// JSON array
|
|
65
|
+
status: (0, import_sqlite_core.text)("status").notNull(),
|
|
66
|
+
// draft, scheduled, published, failed
|
|
67
|
+
scheduledAt: (0, import_sqlite_core.text)("scheduled_at"),
|
|
68
|
+
// ISO string
|
|
69
|
+
publishedAt: (0, import_sqlite_core.text)("published_at"),
|
|
70
|
+
// ISO string
|
|
71
|
+
tags: (0, import_sqlite_core.text)("tags"),
|
|
72
|
+
// JSON array
|
|
73
|
+
mediaIds: (0, import_sqlite_core.text)("media_ids"),
|
|
74
|
+
// JSON array
|
|
75
|
+
templateId: (0, import_sqlite_core.text)("template_id"),
|
|
76
|
+
createdAt: (0, import_sqlite_core.text)("created_at").notNull(),
|
|
77
|
+
updatedAt: (0, import_sqlite_core.text)("updated_at").notNull(),
|
|
78
|
+
createdBy: (0, import_sqlite_core.text)("created_by").notNull()
|
|
79
|
+
});
|
|
80
|
+
var media = (0, import_sqlite_core.sqliteTable)("media", {
|
|
81
|
+
id: (0, import_sqlite_core.text)("id").primaryKey(),
|
|
82
|
+
teamId: (0, import_sqlite_core.text)("team_id").notNull(),
|
|
83
|
+
filename: (0, import_sqlite_core.text)("filename").notNull(),
|
|
84
|
+
originalName: (0, import_sqlite_core.text)("original_name").notNull(),
|
|
85
|
+
mimeType: (0, import_sqlite_core.text)("mime_type").notNull(),
|
|
86
|
+
size: (0, import_sqlite_core.integer)("size").notNull(),
|
|
87
|
+
width: (0, import_sqlite_core.integer)("width"),
|
|
88
|
+
height: (0, import_sqlite_core.integer)("height"),
|
|
89
|
+
alt: (0, import_sqlite_core.text)("alt"),
|
|
90
|
+
tags: (0, import_sqlite_core.text)("tags"),
|
|
91
|
+
// JSON array
|
|
92
|
+
url: (0, import_sqlite_core.text)("url").notNull(),
|
|
93
|
+
thumbnailUrl: (0, import_sqlite_core.text)("thumbnail_url"),
|
|
94
|
+
createdAt: (0, import_sqlite_core.text)("created_at").notNull(),
|
|
95
|
+
createdBy: (0, import_sqlite_core.text)("created_by").notNull()
|
|
96
|
+
});
|
|
97
|
+
var templates = (0, import_sqlite_core.sqliteTable)("templates", {
|
|
98
|
+
id: (0, import_sqlite_core.text)("id").primaryKey(),
|
|
99
|
+
teamId: (0, import_sqlite_core.text)("team_id").notNull(),
|
|
100
|
+
name: (0, import_sqlite_core.text)("name").notNull(),
|
|
101
|
+
content: (0, import_sqlite_core.text)("content").notNull(),
|
|
102
|
+
variables: (0, import_sqlite_core.text)("variables"),
|
|
103
|
+
// JSON array of variable definitions
|
|
104
|
+
tags: (0, import_sqlite_core.text)("tags"),
|
|
105
|
+
// JSON array
|
|
106
|
+
createdAt: (0, import_sqlite_core.text)("created_at").notNull(),
|
|
107
|
+
updatedAt: (0, import_sqlite_core.text)("updated_at").notNull(),
|
|
108
|
+
createdBy: (0, import_sqlite_core.text)("created_by").notNull()
|
|
109
|
+
});
|
|
110
|
+
var socialAccounts = (0, import_sqlite_core.sqliteTable)("social_accounts", {
|
|
111
|
+
id: (0, import_sqlite_core.text)("id").primaryKey(),
|
|
112
|
+
teamId: (0, import_sqlite_core.text)("team_id").notNull(),
|
|
113
|
+
platform: (0, import_sqlite_core.text)("platform").notNull(),
|
|
114
|
+
// twitter, linkedin, instagram, etc.
|
|
115
|
+
displayName: (0, import_sqlite_core.text)("display_name").notNull(),
|
|
116
|
+
username: (0, import_sqlite_core.text)("username"),
|
|
117
|
+
avatar: (0, import_sqlite_core.text)("avatar"),
|
|
118
|
+
isActive: (0, import_sqlite_core.integer)("is_active", { mode: "boolean" }).notNull().default(true),
|
|
119
|
+
credentials: (0, import_sqlite_core.blob)("credentials").notNull(),
|
|
120
|
+
// Encrypted JSON
|
|
121
|
+
settings: (0, import_sqlite_core.text)("settings"),
|
|
122
|
+
// JSON object
|
|
123
|
+
lastSync: (0, import_sqlite_core.text)("last_sync"),
|
|
124
|
+
createdAt: (0, import_sqlite_core.text)("created_at").notNull(),
|
|
125
|
+
updatedAt: (0, import_sqlite_core.text)("updated_at").notNull()
|
|
126
|
+
});
|
|
127
|
+
var postMetrics = (0, import_sqlite_core.sqliteTable)("post_metrics", {
|
|
128
|
+
id: (0, import_sqlite_core.text)("id").primaryKey(),
|
|
129
|
+
postId: (0, import_sqlite_core.text)("post_id").notNull(),
|
|
130
|
+
platform: (0, import_sqlite_core.text)("platform").notNull(),
|
|
131
|
+
impressions: (0, import_sqlite_core.integer)("impressions").default(0),
|
|
132
|
+
likes: (0, import_sqlite_core.integer)("likes").default(0),
|
|
133
|
+
shares: (0, import_sqlite_core.integer)("shares").default(0),
|
|
134
|
+
comments: (0, import_sqlite_core.integer)("comments").default(0),
|
|
135
|
+
clicks: (0, import_sqlite_core.integer)("clicks").default(0),
|
|
136
|
+
engagementRate: (0, import_sqlite_core.text)("engagement_rate"),
|
|
137
|
+
// Stored as string to avoid float precision issues
|
|
138
|
+
syncedAt: (0, import_sqlite_core.text)("synced_at").notNull()
|
|
139
|
+
});
|
|
140
|
+
var accountMetrics = (0, import_sqlite_core.sqliteTable)("account_metrics", {
|
|
141
|
+
id: (0, import_sqlite_core.text)("id").primaryKey(),
|
|
142
|
+
accountId: (0, import_sqlite_core.text)("account_id").notNull(),
|
|
143
|
+
date: (0, import_sqlite_core.text)("date").notNull(),
|
|
144
|
+
// YYYY-MM-DD format
|
|
145
|
+
followers: (0, import_sqlite_core.integer)("followers").default(0),
|
|
146
|
+
following: (0, import_sqlite_core.integer)("following").default(0),
|
|
147
|
+
posts: (0, import_sqlite_core.integer)("posts").default(0),
|
|
148
|
+
engagement: (0, import_sqlite_core.integer)("engagement").default(0),
|
|
149
|
+
reach: (0, import_sqlite_core.integer)("reach").default(0),
|
|
150
|
+
syncedAt: (0, import_sqlite_core.text)("synced_at").notNull()
|
|
151
|
+
});
|
|
152
|
+
var webhooks = (0, import_sqlite_core.sqliteTable)("webhooks", {
|
|
153
|
+
id: (0, import_sqlite_core.text)("id").primaryKey(),
|
|
154
|
+
teamId: (0, import_sqlite_core.text)("team_id").notNull(),
|
|
155
|
+
url: (0, import_sqlite_core.text)("url").notNull(),
|
|
156
|
+
events: (0, import_sqlite_core.text)("events").notNull(),
|
|
157
|
+
// JSON array
|
|
158
|
+
secret: (0, import_sqlite_core.text)("secret"),
|
|
159
|
+
isActive: (0, import_sqlite_core.integer)("is_active", { mode: "boolean" }).notNull().default(true),
|
|
160
|
+
createdAt: (0, import_sqlite_core.text)("created_at").notNull(),
|
|
161
|
+
lastTriggered: (0, import_sqlite_core.text)("last_triggered")
|
|
162
|
+
});
|
|
163
|
+
var postsRelations = (0, import_drizzle_orm.relations)(posts, ({ many }) => ({
|
|
164
|
+
metrics: many(postMetrics)
|
|
165
|
+
}));
|
|
166
|
+
var postMetricsRelations = (0, import_drizzle_orm.relations)(postMetrics, ({ one }) => ({
|
|
167
|
+
post: one(posts, {
|
|
168
|
+
fields: [postMetrics.postId],
|
|
169
|
+
references: [posts.id]
|
|
170
|
+
})
|
|
171
|
+
}));
|
|
172
|
+
var socialAccountsRelations = (0, import_drizzle_orm.relations)(socialAccounts, ({ many }) => ({
|
|
173
|
+
metrics: many(accountMetrics)
|
|
174
|
+
}));
|
|
175
|
+
var accountMetricsRelations = (0, import_drizzle_orm.relations)(accountMetrics, ({ one }) => ({
|
|
176
|
+
account: one(socialAccounts, {
|
|
177
|
+
fields: [accountMetrics.accountId],
|
|
178
|
+
references: [socialAccounts.id]
|
|
179
|
+
})
|
|
180
|
+
}));
|
|
181
|
+
|
|
182
|
+
// src/db/index.ts
|
|
183
|
+
var import_migrator = require("drizzle-orm/better-sqlite3/migrator");
|
|
184
|
+
var import_crypto = require("crypto");
|
|
185
|
+
function createDatabase(teamId) {
|
|
186
|
+
const dbPath = process.env.KITCHEN_PLUGIN_DB_PATH || "./data";
|
|
187
|
+
const teamDbFile = `${dbPath}/marketing-${teamId}.db`;
|
|
188
|
+
const sqlite = new import_better_sqlite3.default(teamDbFile);
|
|
189
|
+
const db = (0, import_better_sqlite32.drizzle)(sqlite, { schema: schema_exports });
|
|
190
|
+
return { db, sqlite };
|
|
191
|
+
}
|
|
192
|
+
var ENCRYPTION_KEY = process.env.KITCHEN_ENCRYPTION_KEY || "fallback-key-change-in-production";
|
|
193
|
+
function encryptCredentials(credentials) {
|
|
194
|
+
const plaintext = JSON.stringify(credentials);
|
|
195
|
+
const hash = (0, import_crypto.createHash)("sha256").update(ENCRYPTION_KEY).digest();
|
|
196
|
+
const cipher = (0, import_crypto.createCipher)("aes-256-cbc", hash);
|
|
197
|
+
let encrypted = cipher.update(plaintext, "utf8", "hex");
|
|
198
|
+
encrypted += cipher.final("hex");
|
|
199
|
+
return Buffer.from(encrypted, "hex");
|
|
200
|
+
}
|
|
201
|
+
function initializeDatabase(teamId) {
|
|
202
|
+
const { db, sqlite } = createDatabase(teamId);
|
|
203
|
+
try {
|
|
204
|
+
(0, import_migrator.migrate)(db, { migrationsFolder: "./db/migrations" });
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.warn("Migration warning:", error.message);
|
|
207
|
+
}
|
|
208
|
+
return { db, sqlite };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/api/handler.ts
|
|
212
|
+
function apiError(status, error, message, details) {
|
|
213
|
+
const payload = { error, message, details };
|
|
214
|
+
return { status, data: payload };
|
|
215
|
+
}
|
|
216
|
+
function parsePagination(query) {
|
|
217
|
+
const limit = Math.min(parseInt(query.limit || "20", 10) || 20, 100);
|
|
218
|
+
const offset = parseInt(query.offset || "0", 10) || 0;
|
|
219
|
+
return { limit, offset };
|
|
220
|
+
}
|
|
221
|
+
function getTeamId(req) {
|
|
222
|
+
return req.query.team || req.query.teamId || req.headers["x-team-id"] || "default";
|
|
223
|
+
}
|
|
224
|
+
function getUserId(req) {
|
|
225
|
+
return req.headers["x-user-id"] || "system";
|
|
226
|
+
}
|
|
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) {
|
|
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
|
+
}
|
|
362
|
+
if (req.path === "/posts" && req.method === "GET") {
|
|
363
|
+
try {
|
|
364
|
+
const { db } = initializeDatabase(teamId);
|
|
365
|
+
const { limit, offset } = parsePagination(req.query);
|
|
366
|
+
const conditions = [(0, import_drizzle_orm2.eq)(posts.teamId, teamId)];
|
|
367
|
+
if (req.query.status) {
|
|
368
|
+
conditions.push((0, import_drizzle_orm2.eq)(posts.status, String(req.query.status)));
|
|
369
|
+
}
|
|
370
|
+
if (req.query.platform) {
|
|
371
|
+
conditions.push((0, import_drizzle_orm2.like)(posts.platforms, `%"${req.query.platform}"%`));
|
|
372
|
+
}
|
|
373
|
+
const totalResult = await db.select({ count: import_drizzle_orm2.sql`count(*)` }).from(posts).where((0, import_drizzle_orm2.and)(...conditions));
|
|
374
|
+
const total = totalResult[0]?.count ?? 0;
|
|
375
|
+
const posts2 = await db.select().from(posts).where((0, import_drizzle_orm2.and)(...conditions)).orderBy((0, import_drizzle_orm2.desc)(posts.createdAt)).limit(limit).offset(offset);
|
|
376
|
+
const transformed = posts2.map((post) => ({
|
|
377
|
+
id: post.id,
|
|
378
|
+
content: post.content,
|
|
379
|
+
platforms: JSON.parse(post.platforms || "[]"),
|
|
380
|
+
status: post.status,
|
|
381
|
+
scheduledAt: post.scheduledAt || void 0,
|
|
382
|
+
publishedAt: post.publishedAt || void 0,
|
|
383
|
+
tags: JSON.parse(post.tags || "[]"),
|
|
384
|
+
mediaIds: JSON.parse(post.mediaIds || "[]"),
|
|
385
|
+
templateId: post.templateId || void 0,
|
|
386
|
+
createdAt: post.createdAt,
|
|
387
|
+
updatedAt: post.updatedAt,
|
|
388
|
+
createdBy: post.createdBy
|
|
389
|
+
}));
|
|
390
|
+
const payload = {
|
|
391
|
+
data: transformed,
|
|
392
|
+
total,
|
|
393
|
+
offset,
|
|
394
|
+
limit,
|
|
395
|
+
hasMore: offset + limit < total
|
|
396
|
+
};
|
|
397
|
+
return { status: 200, data: payload };
|
|
398
|
+
} catch (error) {
|
|
399
|
+
return apiError(500, "DATABASE_ERROR", error?.message || "Unknown error");
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (req.path === "/posts" && req.method === "POST") {
|
|
403
|
+
try {
|
|
404
|
+
const userId = getUserId(req);
|
|
405
|
+
const body = req.body || {};
|
|
406
|
+
if (!body.content || !Array.isArray(body.platforms) || body.platforms.length === 0) {
|
|
407
|
+
return apiError(400, "VALIDATION_ERROR", "Content and platforms are required");
|
|
408
|
+
}
|
|
409
|
+
const { db } = initializeDatabase(teamId);
|
|
410
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
411
|
+
const newPost = {
|
|
412
|
+
id: (0, import_crypto2.randomUUID)(),
|
|
413
|
+
teamId,
|
|
414
|
+
content: body.content,
|
|
415
|
+
platforms: JSON.stringify(body.platforms),
|
|
416
|
+
status: body.status || "draft",
|
|
417
|
+
scheduledAt: body.scheduledAt || null,
|
|
418
|
+
publishedAt: null,
|
|
419
|
+
tags: JSON.stringify(body.tags || []),
|
|
420
|
+
mediaIds: JSON.stringify(body.mediaIds || []),
|
|
421
|
+
templateId: body.templateId || null,
|
|
422
|
+
createdAt: now,
|
|
423
|
+
updatedAt: now,
|
|
424
|
+
createdBy: userId
|
|
425
|
+
};
|
|
426
|
+
await db.insert(posts).values(newPost);
|
|
427
|
+
const response = {
|
|
428
|
+
id: newPost.id,
|
|
429
|
+
content: newPost.content,
|
|
430
|
+
platforms: JSON.parse(newPost.platforms),
|
|
431
|
+
status: newPost.status,
|
|
432
|
+
scheduledAt: newPost.scheduledAt || void 0,
|
|
433
|
+
publishedAt: void 0,
|
|
434
|
+
tags: JSON.parse(newPost.tags),
|
|
435
|
+
mediaIds: JSON.parse(newPost.mediaIds),
|
|
436
|
+
templateId: newPost.templateId || void 0,
|
|
437
|
+
createdAt: newPost.createdAt,
|
|
438
|
+
updatedAt: newPost.updatedAt,
|
|
439
|
+
createdBy: newPost.createdBy
|
|
440
|
+
};
|
|
441
|
+
return { status: 201, data: response };
|
|
442
|
+
} catch (error) {
|
|
443
|
+
return apiError(500, "DATABASE_ERROR", error?.message || "Unknown error");
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (req.path === "/accounts" && req.method === "GET") {
|
|
447
|
+
try {
|
|
448
|
+
const { db } = initializeDatabase(teamId);
|
|
449
|
+
const accounts = await db.select().from(socialAccounts).where((0, import_drizzle_orm2.eq)(socialAccounts.teamId, teamId)).orderBy((0, import_drizzle_orm2.desc)(socialAccounts.createdAt));
|
|
450
|
+
const response = accounts.map((account) => ({
|
|
451
|
+
id: account.id,
|
|
452
|
+
platform: account.platform,
|
|
453
|
+
displayName: account.displayName,
|
|
454
|
+
username: account.username || void 0,
|
|
455
|
+
avatar: account.avatar || void 0,
|
|
456
|
+
isActive: account.isActive,
|
|
457
|
+
settings: JSON.parse(account.settings || "{}"),
|
|
458
|
+
lastSync: account.lastSync || void 0,
|
|
459
|
+
createdAt: account.createdAt,
|
|
460
|
+
updatedAt: account.updatedAt
|
|
461
|
+
}));
|
|
462
|
+
return { status: 200, data: { accounts: response } };
|
|
463
|
+
} catch (error) {
|
|
464
|
+
return apiError(500, "DATABASE_ERROR", error?.message || "Unknown error");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (req.path === "/accounts" && req.method === "POST") {
|
|
468
|
+
try {
|
|
469
|
+
const body = req.body || {};
|
|
470
|
+
if (!body.platform || !body.displayName || !body.credentials) {
|
|
471
|
+
return apiError(400, "VALIDATION_ERROR", "platform, displayName, and credentials are required");
|
|
472
|
+
}
|
|
473
|
+
const { db } = initializeDatabase(teamId);
|
|
474
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
475
|
+
const newAccount = {
|
|
476
|
+
id: (0, import_crypto2.randomUUID)(),
|
|
477
|
+
teamId,
|
|
478
|
+
platform: body.platform,
|
|
479
|
+
displayName: body.displayName,
|
|
480
|
+
username: body.username || null,
|
|
481
|
+
avatar: null,
|
|
482
|
+
isActive: true,
|
|
483
|
+
credentials: encryptCredentials(body.credentials),
|
|
484
|
+
settings: JSON.stringify(body.settings || {}),
|
|
485
|
+
lastSync: null,
|
|
486
|
+
createdAt: now,
|
|
487
|
+
updatedAt: now
|
|
488
|
+
};
|
|
489
|
+
await db.insert(socialAccounts).values(newAccount);
|
|
490
|
+
const response = {
|
|
491
|
+
id: newAccount.id,
|
|
492
|
+
platform: newAccount.platform,
|
|
493
|
+
displayName: newAccount.displayName,
|
|
494
|
+
username: newAccount.username || void 0,
|
|
495
|
+
isActive: newAccount.isActive,
|
|
496
|
+
settings: JSON.parse(newAccount.settings),
|
|
497
|
+
createdAt: newAccount.createdAt,
|
|
498
|
+
updatedAt: newAccount.updatedAt
|
|
499
|
+
};
|
|
500
|
+
return { status: 201, data: response };
|
|
501
|
+
} catch (error) {
|
|
502
|
+
return apiError(500, "DATABASE_ERROR", error?.message || "Unknown error");
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return apiError(501, "NOT_IMPLEMENTED", `No handler for ${req.method} ${req.path}`);
|
|
506
|
+
}
|
|
507
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
508
|
+
0 && (module.exports = {
|
|
509
|
+
handleRequest
|
|
510
|
+
});
|
package/dist/tabs/accounts.js
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
const R = window.React;
|
|
5
5
|
if (!R) return;
|
|
6
6
|
const h = R.createElement;
|
|
7
|
+
const useEffect = R.useEffect;
|
|
8
|
+
const useMemo = R.useMemo;
|
|
9
|
+
const useState = R.useState;
|
|
7
10
|
const t = {
|
|
8
11
|
text: { color: "var(--ck-text-primary)" },
|
|
9
12
|
muted: { color: "var(--ck-text-secondary)" },
|
|
@@ -14,48 +17,406 @@
|
|
|
14
17
|
borderRadius: "10px",
|
|
15
18
|
padding: "1rem"
|
|
16
19
|
},
|
|
17
|
-
|
|
20
|
+
input: {
|
|
18
21
|
background: "rgba(255,255,255,0.03)",
|
|
19
22
|
border: "1px solid var(--ck-border-subtle)",
|
|
20
23
|
borderRadius: "10px",
|
|
24
|
+
padding: "0.6rem 0.75rem",
|
|
25
|
+
color: "var(--ck-text-primary)",
|
|
26
|
+
width: "100%"
|
|
27
|
+
},
|
|
28
|
+
btnPrimary: {
|
|
29
|
+
background: "var(--ck-accent-red)",
|
|
30
|
+
border: "1px solid rgba(255,255,255,0.08)",
|
|
31
|
+
borderRadius: "10px",
|
|
32
|
+
padding: "0.5rem 0.75rem",
|
|
33
|
+
color: "white",
|
|
34
|
+
fontWeight: 700,
|
|
21
35
|
cursor: "pointer",
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
36
|
+
fontSize: "0.8rem"
|
|
37
|
+
},
|
|
38
|
+
btnGhost: {
|
|
39
|
+
background: "rgba(255,255,255,0.03)",
|
|
40
|
+
border: "1px solid var(--ck-border-subtle)",
|
|
41
|
+
borderRadius: "10px",
|
|
42
|
+
padding: "0.5rem 0.75rem",
|
|
43
|
+
color: "var(--ck-text-primary)",
|
|
44
|
+
fontWeight: 600,
|
|
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}"
|
|
26
75
|
};
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
function Accounts() {
|
|
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)"
|
|
81
|
+
};
|
|
82
|
+
function Accounts(props) {
|
|
83
|
+
const teamId = String(props?.teamId || "default");
|
|
84
|
+
const apiBase = useMemo(() => `/api/plugins/marketing`, []);
|
|
85
|
+
const [providers, setProviders] = useState([]);
|
|
86
|
+
const [manualAccounts, setManualAccounts] = useState([]);
|
|
87
|
+
const [loading, setLoading] = useState(true);
|
|
88
|
+
const [detecting, setDetecting] = useState(false);
|
|
89
|
+
const [error, setError] = useState(null);
|
|
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);
|
|
120
|
+
setError(null);
|
|
121
|
+
try {
|
|
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 });
|
|
128
|
+
const json = await res.json();
|
|
129
|
+
setProviders(Array.isArray(json.providers) ? json.providers : []);
|
|
130
|
+
} catch (e) {
|
|
131
|
+
setError(e?.message || "Failed to detect providers");
|
|
132
|
+
} finally {
|
|
133
|
+
setDetecting(false);
|
|
134
|
+
}
|
|
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
|
+
};
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
void refresh();
|
|
151
|
+
}, [teamId]);
|
|
152
|
+
const onManualConnect = async () => {
|
|
153
|
+
setSaving(true);
|
|
154
|
+
setError(null);
|
|
155
|
+
try {
|
|
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();
|
|
172
|
+
} catch (e) {
|
|
173
|
+
setError(e?.message || "Failed to connect");
|
|
174
|
+
} finally {
|
|
175
|
+
setSaving(false);
|
|
176
|
+
}
|
|
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
|
+
};
|
|
34
208
|
return h(
|
|
35
209
|
"div",
|
|
36
210
|
{ className: "space-y-3" },
|
|
211
|
+
// ---- Header ----
|
|
37
212
|
h(
|
|
38
213
|
"div",
|
|
39
214
|
{ style: t.card },
|
|
40
|
-
h("div", { className: "text-sm font-medium mb-3", style: t.text }, "Connect Account"),
|
|
41
215
|
h(
|
|
42
216
|
"div",
|
|
43
|
-
{
|
|
44
|
-
|
|
45
|
-
|
|
217
|
+
{ className: "flex items-start justify-between gap-2" },
|
|
218
|
+
h(
|
|
219
|
+
"div",
|
|
220
|
+
null,
|
|
221
|
+
h("div", { className: "text-sm font-medium", style: t.text }, "Connected Accounts"),
|
|
222
|
+
h(
|
|
46
223
|
"div",
|
|
47
|
-
{
|
|
48
|
-
|
|
49
|
-
h("div", { className: "text-xs font-medium", style: t.text }, p.name)
|
|
224
|
+
{ className: "mt-1 text-xs", style: t.faint },
|
|
225
|
+
`${allProviders.length} provider${allProviders.length !== 1 ? "s" : ""} detected`
|
|
50
226
|
)
|
|
227
|
+
),
|
|
228
|
+
h(
|
|
229
|
+
"div",
|
|
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")
|
|
242
|
+
)
|
|
243
|
+
),
|
|
244
|
+
error && h("div", { className: "mt-2 text-xs", style: { color: "rgba(248,113,113,0.95)" } }, error)
|
|
245
|
+
),
|
|
246
|
+
// ---- Postiz setup ----
|
|
247
|
+
showPostizSetup && h(
|
|
248
|
+
"div",
|
|
249
|
+
{ style: t.card },
|
|
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
|
+
})
|
|
51
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")
|
|
52
288
|
)
|
|
53
289
|
),
|
|
54
|
-
|
|
290
|
+
// ---- Manual account form ----
|
|
291
|
+
showManual && h(
|
|
55
292
|
"div",
|
|
56
293
|
{ style: t.card },
|
|
57
|
-
h("div", { className: "text-sm font-medium mb-2", style: t.text }, "
|
|
58
|
-
h("div", { className: "
|
|
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."),
|
|
296
|
+
h(
|
|
297
|
+
"div",
|
|
298
|
+
{ className: "grid grid-cols-1 gap-2 sm:grid-cols-2" },
|
|
299
|
+
h(
|
|
300
|
+
"div",
|
|
301
|
+
null,
|
|
302
|
+
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "Platform"),
|
|
303
|
+
h(
|
|
304
|
+
"select",
|
|
305
|
+
{ value: manPlatform, onChange: (e) => setManPlatform(e.target.value), style: t.input },
|
|
306
|
+
h("option", { value: "twitter" }, "Twitter / X"),
|
|
307
|
+
h("option", { value: "instagram" }, "Instagram"),
|
|
308
|
+
h("option", { value: "linkedin" }, "LinkedIn"),
|
|
309
|
+
h("option", { value: "bluesky" }, "Bluesky"),
|
|
310
|
+
h("option", { value: "mastodon" }, "Mastodon")
|
|
311
|
+
)
|
|
312
|
+
),
|
|
313
|
+
h(
|
|
314
|
+
"div",
|
|
315
|
+
null,
|
|
316
|
+
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "Display name"),
|
|
317
|
+
h("input", { value: manName, onChange: (e) => setManName(e.target.value), placeholder: "My X account", style: t.input })
|
|
318
|
+
),
|
|
319
|
+
h(
|
|
320
|
+
"div",
|
|
321
|
+
null,
|
|
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 })
|
|
324
|
+
),
|
|
325
|
+
h(
|
|
326
|
+
"div",
|
|
327
|
+
null,
|
|
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 })
|
|
330
|
+
)
|
|
331
|
+
),
|
|
332
|
+
h(
|
|
333
|
+
"div",
|
|
334
|
+
{ className: "mt-3 flex gap-2" },
|
|
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")
|
|
337
|
+
)
|
|
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(
|
|
347
|
+
"div",
|
|
348
|
+
{ style: t.card },
|
|
349
|
+
h(
|
|
350
|
+
"div",
|
|
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(
|
|
375
|
+
"div",
|
|
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
|
|
391
|
+
h(
|
|
392
|
+
"div",
|
|
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
|
+
)
|
|
400
|
+
),
|
|
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
|
+
)
|
|
416
|
+
)
|
|
417
|
+
)
|
|
418
|
+
)
|
|
419
|
+
)
|
|
59
420
|
)
|
|
60
421
|
);
|
|
61
422
|
}
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
const R = window.React;
|
|
5
5
|
if (!R) return;
|
|
6
6
|
const h = R.createElement;
|
|
7
|
+
const useEffect = R.useEffect;
|
|
8
|
+
const useMemo = R.useMemo;
|
|
9
|
+
const useState = R.useState;
|
|
10
|
+
const useCallback = R.useCallback;
|
|
7
11
|
const t = {
|
|
8
12
|
text: { color: "var(--ck-text-primary)" },
|
|
9
13
|
muted: { color: "var(--ck-text-secondary)" },
|
|
@@ -13,31 +17,331 @@
|
|
|
13
17
|
border: "1px solid var(--ck-border-subtle)",
|
|
14
18
|
borderRadius: "10px",
|
|
15
19
|
padding: "1rem"
|
|
20
|
+
},
|
|
21
|
+
input: {
|
|
22
|
+
background: "rgba(255,255,255,0.03)",
|
|
23
|
+
border: "1px solid var(--ck-border-subtle)",
|
|
24
|
+
borderRadius: "10px",
|
|
25
|
+
padding: "0.6rem 0.75rem",
|
|
26
|
+
color: "var(--ck-text-primary)",
|
|
27
|
+
width: "100%"
|
|
28
|
+
},
|
|
29
|
+
btnPrimary: {
|
|
30
|
+
background: "var(--ck-accent-red)",
|
|
31
|
+
border: "1px solid rgba(255,255,255,0.08)",
|
|
32
|
+
borderRadius: "10px",
|
|
33
|
+
padding: "0.6rem 0.85rem",
|
|
34
|
+
color: "white",
|
|
35
|
+
fontWeight: 700,
|
|
36
|
+
cursor: "pointer"
|
|
37
|
+
},
|
|
38
|
+
btnGhost: {
|
|
39
|
+
background: "rgba(255,255,255,0.03)",
|
|
40
|
+
border: "1px solid var(--ck-border-subtle)",
|
|
41
|
+
borderRadius: "10px",
|
|
42
|
+
padding: "0.6rem 0.85rem",
|
|
43
|
+
color: "var(--ck-text-primary)",
|
|
44
|
+
fontWeight: 600,
|
|
45
|
+
cursor: "pointer"
|
|
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
|
+
},
|
|
56
|
+
pill: (active) => ({
|
|
57
|
+
background: active ? "rgba(99,179,237,0.16)" : "rgba(255,255,255,0.03)",
|
|
58
|
+
border: `1px solid ${active ? "rgba(99,179,237,0.45)" : "var(--ck-border-subtle)"}`,
|
|
59
|
+
borderRadius: "999px",
|
|
60
|
+
padding: "0.25rem 0.55rem",
|
|
61
|
+
fontSize: "0.8rem",
|
|
62
|
+
color: active ? "rgba(210,235,255,0.95)" : "var(--ck-text-secondary)",
|
|
63
|
+
cursor: "pointer",
|
|
64
|
+
userSelect: "none"
|
|
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
|
+
};
|
|
16
82
|
}
|
|
17
83
|
};
|
|
18
|
-
function ContentLibrary() {
|
|
84
|
+
function ContentLibrary(props) {
|
|
85
|
+
const teamId = String(props?.teamId || "default");
|
|
86
|
+
const apiBase = useMemo(() => `/api/plugins/marketing`, []);
|
|
87
|
+
const [posts, setPosts] = useState([]);
|
|
88
|
+
const [providers, setProviders] = useState([]);
|
|
89
|
+
const [loading, setLoading] = useState(true);
|
|
90
|
+
const [saving, setSaving] = useState(false);
|
|
91
|
+
const [publishing, setPublishing] = useState(false);
|
|
92
|
+
const [error, setError] = useState(null);
|
|
93
|
+
const [success, setSuccess] = useState(null);
|
|
94
|
+
const [content, setContent] = useState("");
|
|
95
|
+
const [selectedProviders, setSelectedProviders] = useState([]);
|
|
96
|
+
const [scheduledAt, setScheduledAt] = useState("");
|
|
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 () => {
|
|
114
|
+
try {
|
|
115
|
+
const res = await fetch(`${apiBase}/posts?team=${encodeURIComponent(teamId)}&limit=25`);
|
|
116
|
+
const json = await res.json();
|
|
117
|
+
setPosts(Array.isArray(json.data) ? json.data : []);
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
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]);
|
|
130
|
+
useEffect(() => {
|
|
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
|
+
);
|
|
138
|
+
};
|
|
139
|
+
const onSaveDraft = async () => {
|
|
140
|
+
if (!content.trim()) return;
|
|
141
|
+
setSaving(true);
|
|
142
|
+
setError(null);
|
|
143
|
+
try {
|
|
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)}`, {
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: { "content-type": "application/json", ...postizHeaders },
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
content,
|
|
184
|
+
integrationIds,
|
|
185
|
+
scheduledAt: scheduledAt || void 0
|
|
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})`);
|
|
191
|
+
}
|
|
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!");
|
|
197
|
+
}
|
|
198
|
+
setContent("");
|
|
199
|
+
setScheduledAt("");
|
|
200
|
+
setSelectedProviders([]);
|
|
201
|
+
await loadPosts();
|
|
202
|
+
} catch (e) {
|
|
203
|
+
setError(e?.message || "Publish failed");
|
|
204
|
+
} finally {
|
|
205
|
+
setPublishing(false);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
const postizAvailable = providers.some((p) => p.type === "postiz");
|
|
209
|
+
const hasSelection = selectedProviders.length > 0;
|
|
19
210
|
return h(
|
|
20
211
|
"div",
|
|
21
212
|
{ className: "space-y-3" },
|
|
213
|
+
// ---- Composer ----
|
|
22
214
|
h(
|
|
23
215
|
"div",
|
|
24
216
|
{ style: t.card },
|
|
25
|
-
h("div", { className: "text-sm font-medium mb-
|
|
26
|
-
h("div", { className: "text-sm", style: t.muted }, "Your content creation tools will live here:"),
|
|
217
|
+
h("div", { className: "text-sm font-medium mb-3", style: t.text }, "Compose"),
|
|
27
218
|
h(
|
|
28
|
-
"
|
|
29
|
-
{ className: "
|
|
30
|
-
h("
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
219
|
+
"div",
|
|
220
|
+
{ className: "space-y-3" },
|
|
221
|
+
h("textarea", {
|
|
222
|
+
value: content,
|
|
223
|
+
onChange: (e) => setContent(e.target.value),
|
|
224
|
+
placeholder: "Write your post\u2026",
|
|
225
|
+
rows: 5,
|
|
226
|
+
style: { ...t.input, resize: "vertical", minHeight: "110px" }
|
|
227
|
+
}),
|
|
228
|
+
// Provider selector
|
|
229
|
+
providers.length > 0 && h(
|
|
230
|
+
"div",
|
|
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
|
+
)
|
|
249
|
+
)
|
|
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
|
|
258
|
+
h(
|
|
259
|
+
"div",
|
|
260
|
+
{ className: "grid grid-cols-1 gap-2 sm:grid-cols-2" },
|
|
261
|
+
h(
|
|
262
|
+
"div",
|
|
263
|
+
null,
|
|
264
|
+
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "Schedule (optional)"),
|
|
265
|
+
h("input", {
|
|
266
|
+
type: "datetime-local",
|
|
267
|
+
value: scheduledAt,
|
|
268
|
+
onChange: (e) => setScheduledAt(e.target.value),
|
|
269
|
+
style: t.input
|
|
270
|
+
})
|
|
271
|
+
),
|
|
272
|
+
h(
|
|
273
|
+
"div",
|
|
274
|
+
{ className: "flex items-end" },
|
|
275
|
+
h(
|
|
276
|
+
"div",
|
|
277
|
+
{ className: "text-xs", style: t.faint },
|
|
278
|
+
content.length > 0 ? `${content.length} chars` : ""
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
),
|
|
282
|
+
// Actions
|
|
283
|
+
h(
|
|
284
|
+
"div",
|
|
285
|
+
{ className: "flex flex-wrap gap-2 items-center" },
|
|
286
|
+
h("button", {
|
|
287
|
+
type: "button",
|
|
288
|
+
onClick: () => void onSaveDraft(),
|
|
289
|
+
style: { ...t.btnGhost, opacity: saving ? 0.7 : 1 },
|
|
290
|
+
disabled: saving || !content.trim()
|
|
291
|
+
}, saving ? "Saving\u2026" : "Save draft"),
|
|
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
|
+
)
|
|
303
|
+
),
|
|
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)
|
|
34
306
|
)
|
|
35
307
|
),
|
|
308
|
+
// ---- Posts list ----
|
|
36
309
|
h(
|
|
37
310
|
"div",
|
|
38
311
|
{ style: t.card },
|
|
39
|
-
h(
|
|
40
|
-
|
|
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
|
+
),
|
|
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(
|
|
319
|
+
"div",
|
|
320
|
+
{ className: "space-y-2" },
|
|
321
|
+
...posts.map(
|
|
322
|
+
(p) => h(
|
|
323
|
+
"div",
|
|
324
|
+
{ key: p.id, style: { ...t.card, padding: "0.75rem" } },
|
|
325
|
+
h(
|
|
326
|
+
"div",
|
|
327
|
+
{ className: "flex items-center justify-between gap-2" },
|
|
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()}`)
|
|
335
|
+
),
|
|
336
|
+
h("div", { className: "mt-2 whitespace-pre-wrap text-sm", style: t.text }, p.content),
|
|
337
|
+
p.platforms?.length > 0 && h(
|
|
338
|
+
"div",
|
|
339
|
+
{ className: "mt-2 flex flex-wrap gap-1" },
|
|
340
|
+
...p.platforms.map((pl) => h("span", { key: pl, style: t.pill(true) }, pl))
|
|
341
|
+
)
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
)
|
|
41
345
|
)
|
|
42
346
|
);
|
|
43
347
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jiggai/kitchen-plugin-marketing",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Marketing Suite plugin for ClawKitchen",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"bundle": "./dist/tabs/accounts.js"
|
|
46
46
|
}
|
|
47
47
|
],
|
|
48
|
-
"apiRoutes": "./dist/api/
|
|
48
|
+
"apiRoutes": "./dist/api/handler.js",
|
|
49
49
|
"migrations": "./db/migrations"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|