@jiggai/kitchen-plugin-marketing 0.2.5 → 0.2.6
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 +377 -0
- package/dist/tabs/accounts.js +185 -23
- package/dist/tabs/content-library.js +192 -12
- package/package.json +2 -2
|
@@ -0,0 +1,377 @@
|
|
|
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
|
+
async function handleRequest(req, _ctx) {
|
|
228
|
+
const teamId = getTeamId(req);
|
|
229
|
+
if (req.path === "/posts" && req.method === "GET") {
|
|
230
|
+
try {
|
|
231
|
+
const { db } = initializeDatabase(teamId);
|
|
232
|
+
const { limit, offset } = parsePagination(req.query);
|
|
233
|
+
const conditions = [(0, import_drizzle_orm2.eq)(posts.teamId, teamId)];
|
|
234
|
+
if (req.query.status) {
|
|
235
|
+
conditions.push((0, import_drizzle_orm2.eq)(posts.status, String(req.query.status)));
|
|
236
|
+
}
|
|
237
|
+
if (req.query.platform) {
|
|
238
|
+
conditions.push((0, import_drizzle_orm2.like)(posts.platforms, `%"${req.query.platform}"%`));
|
|
239
|
+
}
|
|
240
|
+
const totalResult = await db.select({ count: import_drizzle_orm2.sql`count(*)` }).from(posts).where((0, import_drizzle_orm2.and)(...conditions));
|
|
241
|
+
const total = totalResult[0]?.count ?? 0;
|
|
242
|
+
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);
|
|
243
|
+
const transformed = posts2.map((post) => ({
|
|
244
|
+
id: post.id,
|
|
245
|
+
content: post.content,
|
|
246
|
+
platforms: JSON.parse(post.platforms || "[]"),
|
|
247
|
+
status: post.status,
|
|
248
|
+
scheduledAt: post.scheduledAt || void 0,
|
|
249
|
+
publishedAt: post.publishedAt || void 0,
|
|
250
|
+
tags: JSON.parse(post.tags || "[]"),
|
|
251
|
+
mediaIds: JSON.parse(post.mediaIds || "[]"),
|
|
252
|
+
templateId: post.templateId || void 0,
|
|
253
|
+
createdAt: post.createdAt,
|
|
254
|
+
updatedAt: post.updatedAt,
|
|
255
|
+
createdBy: post.createdBy
|
|
256
|
+
}));
|
|
257
|
+
const payload = {
|
|
258
|
+
data: transformed,
|
|
259
|
+
total,
|
|
260
|
+
offset,
|
|
261
|
+
limit,
|
|
262
|
+
hasMore: offset + limit < total
|
|
263
|
+
};
|
|
264
|
+
return { status: 200, data: payload };
|
|
265
|
+
} catch (error) {
|
|
266
|
+
return apiError(500, "DATABASE_ERROR", error?.message || "Unknown error");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (req.path === "/posts" && req.method === "POST") {
|
|
270
|
+
try {
|
|
271
|
+
const userId = getUserId(req);
|
|
272
|
+
const body = req.body || {};
|
|
273
|
+
if (!body.content || !Array.isArray(body.platforms) || body.platforms.length === 0) {
|
|
274
|
+
return apiError(400, "VALIDATION_ERROR", "Content and platforms are required");
|
|
275
|
+
}
|
|
276
|
+
const { db } = initializeDatabase(teamId);
|
|
277
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
278
|
+
const newPost = {
|
|
279
|
+
id: (0, import_crypto2.randomUUID)(),
|
|
280
|
+
teamId,
|
|
281
|
+
content: body.content,
|
|
282
|
+
platforms: JSON.stringify(body.platforms),
|
|
283
|
+
status: body.status || "draft",
|
|
284
|
+
scheduledAt: body.scheduledAt || null,
|
|
285
|
+
publishedAt: null,
|
|
286
|
+
tags: JSON.stringify(body.tags || []),
|
|
287
|
+
mediaIds: JSON.stringify(body.mediaIds || []),
|
|
288
|
+
templateId: body.templateId || null,
|
|
289
|
+
createdAt: now,
|
|
290
|
+
updatedAt: now,
|
|
291
|
+
createdBy: userId
|
|
292
|
+
};
|
|
293
|
+
await db.insert(posts).values(newPost);
|
|
294
|
+
const response = {
|
|
295
|
+
id: newPost.id,
|
|
296
|
+
content: newPost.content,
|
|
297
|
+
platforms: JSON.parse(newPost.platforms),
|
|
298
|
+
status: newPost.status,
|
|
299
|
+
scheduledAt: newPost.scheduledAt || void 0,
|
|
300
|
+
publishedAt: void 0,
|
|
301
|
+
tags: JSON.parse(newPost.tags),
|
|
302
|
+
mediaIds: JSON.parse(newPost.mediaIds),
|
|
303
|
+
templateId: newPost.templateId || void 0,
|
|
304
|
+
createdAt: newPost.createdAt,
|
|
305
|
+
updatedAt: newPost.updatedAt,
|
|
306
|
+
createdBy: newPost.createdBy
|
|
307
|
+
};
|
|
308
|
+
return { status: 201, data: response };
|
|
309
|
+
} catch (error) {
|
|
310
|
+
return apiError(500, "DATABASE_ERROR", error?.message || "Unknown error");
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (req.path === "/accounts" && req.method === "GET") {
|
|
314
|
+
try {
|
|
315
|
+
const { db } = initializeDatabase(teamId);
|
|
316
|
+
const accounts = await db.select().from(socialAccounts).where((0, import_drizzle_orm2.eq)(socialAccounts.teamId, teamId)).orderBy((0, import_drizzle_orm2.desc)(socialAccounts.createdAt));
|
|
317
|
+
const response = accounts.map((account) => ({
|
|
318
|
+
id: account.id,
|
|
319
|
+
platform: account.platform,
|
|
320
|
+
displayName: account.displayName,
|
|
321
|
+
username: account.username || void 0,
|
|
322
|
+
avatar: account.avatar || void 0,
|
|
323
|
+
isActive: account.isActive,
|
|
324
|
+
settings: JSON.parse(account.settings || "{}"),
|
|
325
|
+
lastSync: account.lastSync || void 0,
|
|
326
|
+
createdAt: account.createdAt,
|
|
327
|
+
updatedAt: account.updatedAt
|
|
328
|
+
}));
|
|
329
|
+
return { status: 200, data: { accounts: response } };
|
|
330
|
+
} catch (error) {
|
|
331
|
+
return apiError(500, "DATABASE_ERROR", error?.message || "Unknown error");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (req.path === "/accounts" && req.method === "POST") {
|
|
335
|
+
try {
|
|
336
|
+
const body = req.body || {};
|
|
337
|
+
if (!body.platform || !body.displayName || !body.credentials) {
|
|
338
|
+
return apiError(400, "VALIDATION_ERROR", "platform, displayName, and credentials are required");
|
|
339
|
+
}
|
|
340
|
+
const { db } = initializeDatabase(teamId);
|
|
341
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
342
|
+
const newAccount = {
|
|
343
|
+
id: (0, import_crypto2.randomUUID)(),
|
|
344
|
+
teamId,
|
|
345
|
+
platform: body.platform,
|
|
346
|
+
displayName: body.displayName,
|
|
347
|
+
username: body.username || null,
|
|
348
|
+
avatar: null,
|
|
349
|
+
isActive: true,
|
|
350
|
+
credentials: encryptCredentials(body.credentials),
|
|
351
|
+
settings: JSON.stringify(body.settings || {}),
|
|
352
|
+
lastSync: null,
|
|
353
|
+
createdAt: now,
|
|
354
|
+
updatedAt: now
|
|
355
|
+
};
|
|
356
|
+
await db.insert(socialAccounts).values(newAccount);
|
|
357
|
+
const response = {
|
|
358
|
+
id: newAccount.id,
|
|
359
|
+
platform: newAccount.platform,
|
|
360
|
+
displayName: newAccount.displayName,
|
|
361
|
+
username: newAccount.username || void 0,
|
|
362
|
+
isActive: newAccount.isActive,
|
|
363
|
+
settings: JSON.parse(newAccount.settings),
|
|
364
|
+
createdAt: newAccount.createdAt,
|
|
365
|
+
updatedAt: newAccount.updatedAt
|
|
366
|
+
};
|
|
367
|
+
return { status: 201, data: response };
|
|
368
|
+
} catch (error) {
|
|
369
|
+
return apiError(500, "DATABASE_ERROR", error?.message || "Unknown error");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return apiError(501, "NOT_IMPLEMENTED", `No handler for ${req.method} ${req.path}`);
|
|
373
|
+
}
|
|
374
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
375
|
+
0 && (module.exports = {
|
|
376
|
+
handleRequest
|
|
377
|
+
});
|
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,207 @@
|
|
|
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",
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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.6rem 0.85rem",
|
|
33
|
+
color: "white",
|
|
34
|
+
fontWeight: 700,
|
|
35
|
+
cursor: "pointer"
|
|
36
|
+
},
|
|
37
|
+
btnGhost: {
|
|
38
|
+
background: "rgba(255,255,255,0.03)",
|
|
39
|
+
border: "1px solid var(--ck-border-subtle)",
|
|
40
|
+
borderRadius: "10px",
|
|
41
|
+
padding: "0.6rem 0.85rem",
|
|
42
|
+
color: "var(--ck-text-primary)",
|
|
43
|
+
fontWeight: 600,
|
|
44
|
+
cursor: "pointer"
|
|
25
45
|
}
|
|
26
46
|
};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
47
|
+
function Accounts(props) {
|
|
48
|
+
const teamId = String(props?.teamId || "default");
|
|
49
|
+
const apiBase = useMemo(() => `/api/plugins/marketing`, []);
|
|
50
|
+
const [accounts, setAccounts] = useState([]);
|
|
51
|
+
const [loading, setLoading] = useState(true);
|
|
52
|
+
const [open, setOpen] = useState(false);
|
|
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);
|
|
58
|
+
const [error, setError] = useState(null);
|
|
59
|
+
const refresh = async () => {
|
|
60
|
+
setLoading(true);
|
|
61
|
+
setError(null);
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(`${apiBase}/accounts?team=${encodeURIComponent(teamId)}`);
|
|
64
|
+
const json = await res.json();
|
|
65
|
+
setAccounts(Array.isArray(json.accounts) ? json.accounts : []);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
setError(e?.message || "Failed to load accounts");
|
|
68
|
+
} finally {
|
|
69
|
+
setLoading(false);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
void refresh();
|
|
74
|
+
}, [teamId]);
|
|
75
|
+
const onConnect = async () => {
|
|
76
|
+
setSaving(true);
|
|
77
|
+
setError(null);
|
|
78
|
+
try {
|
|
79
|
+
const res = await fetch(
|
|
80
|
+
`${apiBase}/accounts?team=${encodeURIComponent(teamId)}`,
|
|
81
|
+
{
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { "content-type": "application/json" },
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
platform,
|
|
86
|
+
displayName: displayName || `${platform} account`,
|
|
87
|
+
username: username || void 0,
|
|
88
|
+
credentials: { accessToken },
|
|
89
|
+
settings: {}
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
const err = await res.json().catch(() => null);
|
|
95
|
+
throw new Error(err?.message || `Connect failed (${res.status})`);
|
|
96
|
+
}
|
|
97
|
+
setOpen(false);
|
|
98
|
+
setDisplayName("");
|
|
99
|
+
setUsername("");
|
|
100
|
+
setAccessToken("");
|
|
101
|
+
await refresh();
|
|
102
|
+
} catch (e) {
|
|
103
|
+
setError(e?.message || "Failed to connect account");
|
|
104
|
+
} finally {
|
|
105
|
+
setSaving(false);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
34
108
|
return h(
|
|
35
109
|
"div",
|
|
36
110
|
{ className: "space-y-3" },
|
|
37
111
|
h(
|
|
38
112
|
"div",
|
|
39
113
|
{ style: t.card },
|
|
40
|
-
h("div", { className: "text-sm font-medium mb-3", style: t.text }, "Connect Account"),
|
|
41
114
|
h(
|
|
42
115
|
"div",
|
|
43
|
-
{
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
116
|
+
{ className: "flex items-start justify-between gap-2" },
|
|
117
|
+
h(
|
|
118
|
+
"div",
|
|
119
|
+
null,
|
|
120
|
+
h("div", { className: "text-sm font-medium", style: t.text }, "Accounts"),
|
|
121
|
+
h("div", { className: "mt-1 text-xs", style: t.faint }, "OAuth flows next. For now this stores an access token placeholder per team.")
|
|
122
|
+
),
|
|
123
|
+
h(
|
|
124
|
+
"div",
|
|
125
|
+
{ className: "flex gap-2" },
|
|
126
|
+
h("button", { type: "button", onClick: () => void refresh(), style: t.btnGhost }, "Refresh"),
|
|
127
|
+
h("button", { type: "button", onClick: () => setOpen(true), style: t.btnPrimary }, "Connect")
|
|
128
|
+
)
|
|
129
|
+
),
|
|
130
|
+
error ? h("div", { className: "mt-2 text-sm", style: { color: "rgba(248,113,113,0.95)" } }, error) : null
|
|
131
|
+
),
|
|
132
|
+
open ? h(
|
|
133
|
+
"div",
|
|
134
|
+
{ style: t.card },
|
|
135
|
+
h("div", { className: "text-sm font-medium mb-3", style: t.text }, "Connect account"),
|
|
136
|
+
h(
|
|
137
|
+
"div",
|
|
138
|
+
{ className: "grid grid-cols-1 gap-2 sm:grid-cols-2" },
|
|
139
|
+
h(
|
|
140
|
+
"div",
|
|
141
|
+
null,
|
|
142
|
+
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "Platform"),
|
|
143
|
+
h(
|
|
144
|
+
"select",
|
|
145
|
+
{
|
|
146
|
+
value: platform,
|
|
147
|
+
onChange: (e) => setPlatform(e.target.value),
|
|
148
|
+
style: t.input
|
|
149
|
+
},
|
|
150
|
+
h("option", { value: "twitter" }, "Twitter / X"),
|
|
151
|
+
h("option", { value: "instagram" }, "Instagram"),
|
|
152
|
+
h("option", { value: "linkedin" }, "LinkedIn")
|
|
50
153
|
)
|
|
154
|
+
),
|
|
155
|
+
h(
|
|
156
|
+
"div",
|
|
157
|
+
null,
|
|
158
|
+
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
|
+
})
|
|
165
|
+
),
|
|
166
|
+
h(
|
|
167
|
+
"div",
|
|
168
|
+
null,
|
|
169
|
+
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "Username (optional)"),
|
|
170
|
+
h("input", {
|
|
171
|
+
value: username,
|
|
172
|
+
onChange: (e) => setUsername(e.target.value),
|
|
173
|
+
placeholder: "e.g. @handle",
|
|
174
|
+
style: t.input
|
|
175
|
+
})
|
|
176
|
+
),
|
|
177
|
+
h(
|
|
178
|
+
"div",
|
|
179
|
+
null,
|
|
180
|
+
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "Access token (placeholder)"),
|
|
181
|
+
h("input", {
|
|
182
|
+
value: accessToken,
|
|
183
|
+
onChange: (e) => setAccessToken(e.target.value),
|
|
184
|
+
placeholder: "token\u2026",
|
|
185
|
+
style: t.input
|
|
186
|
+
})
|
|
51
187
|
)
|
|
188
|
+
),
|
|
189
|
+
h(
|
|
190
|
+
"div",
|
|
191
|
+
{ className: "mt-3 flex gap-2" },
|
|
192
|
+
h("button", { type: "button", onClick: () => setOpen(false), style: t.btnGhost }, "Cancel"),
|
|
193
|
+
h("button", { type: "button", onClick: () => void onConnect(), style: { ...t.btnPrimary, opacity: saving ? 0.7 : 1 }, disabled: saving }, saving ? "Connecting\u2026" : "Save")
|
|
52
194
|
)
|
|
53
|
-
),
|
|
195
|
+
) : null,
|
|
54
196
|
h(
|
|
55
197
|
"div",
|
|
56
198
|
{ style: t.card },
|
|
57
|
-
h("div", { className: "text-sm font-medium mb-2", style: t.text }, "Connected
|
|
58
|
-
h("div", { className: "py-6 text-center text-sm", style: t.faint }, "No accounts connected yet.")
|
|
199
|
+
h("div", { className: "text-sm font-medium mb-2", style: t.text }, "Connected"),
|
|
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(
|
|
201
|
+
"div",
|
|
202
|
+
{ className: "space-y-2" },
|
|
203
|
+
...accounts.map(
|
|
204
|
+
(a) => h(
|
|
205
|
+
"div",
|
|
206
|
+
{ key: a.id, style: { ...t.card, padding: "0.75rem" } },
|
|
207
|
+
h(
|
|
208
|
+
"div",
|
|
209
|
+
{ className: "flex items-center justify-between gap-2" },
|
|
210
|
+
h(
|
|
211
|
+
"div",
|
|
212
|
+
null,
|
|
213
|
+
h("div", { className: "text-sm font-medium", style: t.text }, a.displayName),
|
|
214
|
+
h("div", { className: "text-xs", style: t.faint }, `${a.platform}${a.username ? ` \xB7 ${a.username}` : ""}`)
|
|
215
|
+
),
|
|
216
|
+
h("div", { className: "text-xs", style: a.isActive ? t.muted : t.faint }, a.isActive ? "active" : "disabled")
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
)
|
|
59
221
|
)
|
|
60
222
|
);
|
|
61
223
|
}
|
|
@@ -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)" },
|
|
@@ -13,31 +16,208 @@
|
|
|
13
16
|
border: "1px solid var(--ck-border-subtle)",
|
|
14
17
|
borderRadius: "10px",
|
|
15
18
|
padding: "1rem"
|
|
16
|
-
}
|
|
19
|
+
},
|
|
20
|
+
input: {
|
|
21
|
+
background: "rgba(255,255,255,0.03)",
|
|
22
|
+
border: "1px solid var(--ck-border-subtle)",
|
|
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.6rem 0.85rem",
|
|
33
|
+
color: "white",
|
|
34
|
+
fontWeight: 700,
|
|
35
|
+
cursor: "pointer"
|
|
36
|
+
},
|
|
37
|
+
btnGhost: {
|
|
38
|
+
background: "rgba(255,255,255,0.03)",
|
|
39
|
+
border: "1px solid var(--ck-border-subtle)",
|
|
40
|
+
borderRadius: "10px",
|
|
41
|
+
padding: "0.6rem 0.85rem",
|
|
42
|
+
color: "var(--ck-text-primary)",
|
|
43
|
+
fontWeight: 600,
|
|
44
|
+
cursor: "pointer"
|
|
45
|
+
},
|
|
46
|
+
pill: (active) => ({
|
|
47
|
+
background: active ? "rgba(99,179,237,0.16)" : "rgba(255,255,255,0.03)",
|
|
48
|
+
border: `1px solid ${active ? "rgba(99,179,237,0.45)" : "var(--ck-border-subtle)"}`,
|
|
49
|
+
borderRadius: "999px",
|
|
50
|
+
padding: "0.25rem 0.55rem",
|
|
51
|
+
fontSize: "0.8rem",
|
|
52
|
+
color: active ? "rgba(210,235,255,0.95)" : "var(--ck-text-secondary)",
|
|
53
|
+
cursor: "pointer",
|
|
54
|
+
userSelect: "none"
|
|
55
|
+
})
|
|
17
56
|
};
|
|
18
|
-
function ContentLibrary() {
|
|
57
|
+
function ContentLibrary(props) {
|
|
58
|
+
const teamId = String(props?.teamId || "default");
|
|
59
|
+
const [posts, setPosts] = useState([]);
|
|
60
|
+
const [loading, setLoading] = useState(true);
|
|
61
|
+
const [saving, setSaving] = useState(false);
|
|
62
|
+
const [error, setError] = useState(null);
|
|
63
|
+
const [content, setContent] = useState("");
|
|
64
|
+
const [platforms, setPlatforms] = useState(["twitter"]);
|
|
65
|
+
const [scheduledAt, setScheduledAt] = useState("");
|
|
66
|
+
const status = useMemo(() => scheduledAt ? "scheduled" : "draft", [scheduledAt]);
|
|
67
|
+
const apiBase = useMemo(() => `/api/plugins/marketing`, []);
|
|
68
|
+
const refresh = async () => {
|
|
69
|
+
setLoading(true);
|
|
70
|
+
setError(null);
|
|
71
|
+
try {
|
|
72
|
+
const res = await fetch(`${apiBase}/posts?team=${encodeURIComponent(teamId)}&limit=25&offset=0`);
|
|
73
|
+
const json = await res.json();
|
|
74
|
+
setPosts(Array.isArray(json.data) ? json.data : []);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
setError(e?.message || "Failed to load posts");
|
|
77
|
+
} finally {
|
|
78
|
+
setLoading(false);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
void refresh();
|
|
83
|
+
}, [teamId]);
|
|
84
|
+
const togglePlatform = (p) => {
|
|
85
|
+
setPlatforms((prev) => {
|
|
86
|
+
if (prev.includes(p)) return prev.filter((x) => x !== p);
|
|
87
|
+
return [...prev, p];
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
const onCreate = async () => {
|
|
91
|
+
setSaving(true);
|
|
92
|
+
setError(null);
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(
|
|
95
|
+
`${apiBase}/posts?team=${encodeURIComponent(teamId)}`,
|
|
96
|
+
{
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: { "content-type": "application/json" },
|
|
99
|
+
body: JSON.stringify({
|
|
100
|
+
content,
|
|
101
|
+
platforms,
|
|
102
|
+
status,
|
|
103
|
+
scheduledAt: scheduledAt || void 0
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
const err = await res.json().catch(() => null);
|
|
109
|
+
throw new Error(err?.message || `Create failed (${res.status})`);
|
|
110
|
+
}
|
|
111
|
+
setContent("");
|
|
112
|
+
setScheduledAt("");
|
|
113
|
+
await refresh();
|
|
114
|
+
} catch (e) {
|
|
115
|
+
setError(e?.message || "Failed to create post");
|
|
116
|
+
} finally {
|
|
117
|
+
setSaving(false);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
19
120
|
return h(
|
|
20
121
|
"div",
|
|
21
122
|
{ className: "space-y-3" },
|
|
22
123
|
h(
|
|
23
124
|
"div",
|
|
24
125
|
{ 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:"),
|
|
126
|
+
h("div", { className: "text-sm font-medium mb-3", style: t.text }, "New Post"),
|
|
27
127
|
h(
|
|
28
|
-
"
|
|
29
|
-
{ className: "
|
|
30
|
-
h("
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
128
|
+
"div",
|
|
129
|
+
{ className: "space-y-2" },
|
|
130
|
+
h("textarea", {
|
|
131
|
+
value: content,
|
|
132
|
+
onChange: (e) => setContent(e.target.value),
|
|
133
|
+
placeholder: "Write your post\u2026",
|
|
134
|
+
rows: 5,
|
|
135
|
+
style: { ...t.input, resize: "vertical", minHeight: "110px" }
|
|
136
|
+
}),
|
|
137
|
+
h(
|
|
138
|
+
"div",
|
|
139
|
+
{ className: "flex flex-wrap gap-2 items-center" },
|
|
140
|
+
h("div", { className: "text-xs font-medium", style: t.faint }, "Platforms"),
|
|
141
|
+
["twitter", "instagram", "linkedin"].map(
|
|
142
|
+
(p) => h("span", {
|
|
143
|
+
key: p,
|
|
144
|
+
onClick: () => togglePlatform(p),
|
|
145
|
+
style: t.pill(platforms.includes(p)),
|
|
146
|
+
role: "button",
|
|
147
|
+
tabIndex: 0
|
|
148
|
+
}, p)
|
|
149
|
+
)
|
|
150
|
+
),
|
|
151
|
+
h(
|
|
152
|
+
"div",
|
|
153
|
+
{ className: "grid grid-cols-1 gap-2 sm:grid-cols-2" },
|
|
154
|
+
h(
|
|
155
|
+
"div",
|
|
156
|
+
null,
|
|
157
|
+
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "Schedule (optional)"),
|
|
158
|
+
h("input", {
|
|
159
|
+
type: "datetime-local",
|
|
160
|
+
value: scheduledAt,
|
|
161
|
+
onChange: (e) => setScheduledAt(e.target.value),
|
|
162
|
+
style: t.input
|
|
163
|
+
})
|
|
164
|
+
),
|
|
165
|
+
h(
|
|
166
|
+
"div",
|
|
167
|
+
null,
|
|
168
|
+
h("div", { className: "text-xs font-medium mb-1", style: t.faint }, "Status"),
|
|
169
|
+
h("div", { className: "text-sm", style: t.muted }, status)
|
|
170
|
+
)
|
|
171
|
+
),
|
|
172
|
+
h(
|
|
173
|
+
"div",
|
|
174
|
+
{ className: "flex flex-wrap gap-2 items-center" },
|
|
175
|
+
h("button", {
|
|
176
|
+
type: "button",
|
|
177
|
+
onClick: () => void refresh(),
|
|
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
|
|
186
|
+
}, saving ? "Saving\u2026" : "Save draft"),
|
|
187
|
+
h("div", { className: "text-xs", style: t.faint }, "Media embedding + templates next.")
|
|
188
|
+
),
|
|
189
|
+
error ? h("div", {
|
|
190
|
+
className: "text-sm mt-2",
|
|
191
|
+
style: { color: "rgba(248,113,113,0.95)" }
|
|
192
|
+
}, error) : null
|
|
34
193
|
)
|
|
35
194
|
),
|
|
36
195
|
h(
|
|
37
196
|
"div",
|
|
38
197
|
{ style: t.card },
|
|
39
|
-
h("div", { className: "text-sm font-medium mb-2", style: t.text }, "
|
|
40
|
-
h("div", { className: "py-6 text-center text-sm", style: t.faint }, "
|
|
198
|
+
h("div", { className: "text-sm font-medium mb-2", style: t.text }, "Posts"),
|
|
199
|
+
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
|
+
"div",
|
|
201
|
+
{ className: "space-y-2" },
|
|
202
|
+
...posts.map(
|
|
203
|
+
(p) => h(
|
|
204
|
+
"div",
|
|
205
|
+
{ key: p.id, style: { ...t.card, padding: "0.75rem" } },
|
|
206
|
+
h(
|
|
207
|
+
"div",
|
|
208
|
+
{ className: "flex items-center justify-between gap-2" },
|
|
209
|
+
h("div", { className: "text-xs font-medium", style: t.faint }, new Date(p.createdAt).toLocaleString()),
|
|
210
|
+
h("div", { className: "text-xs", style: t.muted }, `${p.status}${p.scheduledAt ? ` \xB7 ${p.scheduledAt}` : ""}`)
|
|
211
|
+
),
|
|
212
|
+
h("div", { className: "mt-2 whitespace-pre-wrap text-sm", style: t.text }, p.content),
|
|
213
|
+
h(
|
|
214
|
+
"div",
|
|
215
|
+
{ className: "mt-2 flex flex-wrap gap-2" },
|
|
216
|
+
...(p.platforms || []).map((pl) => h("span", { key: pl, style: t.pill(true) }, pl))
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
)
|
|
41
221
|
)
|
|
42
222
|
);
|
|
43
223
|
}
|
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.6",
|
|
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": {
|