@jiggai/kitchen-plugin-marketing 0.2.4 → 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.
@@ -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
+ });
@@ -4,75 +4,219 @@
4
4
  const R = window.React;
5
5
  if (!R) return;
6
6
  const h = R.createElement;
7
- const theme = {
7
+ const useEffect = R.useEffect;
8
+ const useMemo = R.useMemo;
9
+ const useState = R.useState;
10
+ const t = {
8
11
  text: { color: "var(--ck-text-primary)" },
9
- textMuted: { color: "var(--ck-text-secondary)" },
10
- textFaint: { color: "var(--ck-text-tertiary)" },
12
+ muted: { color: "var(--ck-text-secondary)" },
13
+ faint: { color: "var(--ck-text-tertiary)" },
11
14
  card: {
12
- background: "var(--ck-bg-glass)",
15
+ background: "rgba(255,255,255,0.03)",
13
16
  border: "1px solid var(--ck-border-subtle)",
14
- borderRadius: "14px",
15
- backdropFilter: "blur(18px) saturate(1.25)"
17
+ borderRadius: "10px",
18
+ padding: "1rem"
16
19
  },
17
- banner: {
18
- background: "var(--ck-bg-glass)",
19
- border: "1px solid rgba(237,199,80,0.25)",
20
- borderRadius: "14px",
21
- color: "rgba(237,199,80,0.9)",
22
- backdropFilter: "blur(18px)"
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"
23
36
  },
24
- platformBtn: {
25
- background: "var(--ck-bg-glass)",
37
+ btnGhost: {
38
+ background: "rgba(255,255,255,0.03)",
26
39
  border: "1px solid var(--ck-border-subtle)",
27
- borderRadius: "12px",
28
- cursor: "pointer",
29
- backdropFilter: "blur(18px)",
30
- transition: "border-color 0.15s, background 0.15s"
40
+ borderRadius: "10px",
41
+ padding: "0.6rem 0.85rem",
42
+ color: "var(--ck-text-primary)",
43
+ fontWeight: 600,
44
+ cursor: "pointer"
31
45
  }
32
46
  };
33
- const platforms = [
34
- { icon: "\u{1D54F}", name: "Twitter / X" },
35
- { icon: "\u{1F4F7}", name: "Instagram" },
36
- { icon: "\u{1F3AC}", name: "YouTube" },
37
- { icon: "\u{1F4BC}", name: "LinkedIn" }
38
- ];
39
- function Accounts() {
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
+ };
40
108
  return h(
41
109
  "div",
42
- { className: "p-8 max-w-5xl mx-auto" },
43
- h("h2", { className: "text-2xl font-bold mb-6", style: theme.text }, "Social Media Accounts"),
44
- h("div", { className: "p-4 mb-6 text-sm", style: theme.banner }, "\u{1F517} Connect and manage your social media accounts"),
110
+ { className: "space-y-3" },
45
111
  h(
46
112
  "div",
47
- { className: "p-6 mb-4", style: theme.card },
48
- h("h3", { className: "text-lg font-semibold mb-4", style: theme.text }, "Add New Account"),
113
+ { style: t.card },
49
114
  h(
50
115
  "div",
51
- { className: "grid grid-cols-4 gap-3" },
52
- ...platforms.map(
53
- (p) => h(
54
- "div",
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",
55
145
  {
56
- key: p.name,
57
- className: "p-5 text-center",
58
- style: theme.platformBtn
146
+ value: platform,
147
+ onChange: (e) => setPlatform(e.target.value),
148
+ style: t.input
59
149
  },
60
- h("div", { className: "text-2xl mb-2" }, p.icon),
61
- h("div", { className: "text-sm font-medium", style: theme.text }, p.name)
150
+ h("option", { value: "twitter" }, "Twitter / X"),
151
+ h("option", { value: "instagram" }, "Instagram"),
152
+ h("option", { value: "linkedin" }, "LinkedIn")
62
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
+ })
63
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")
64
194
  )
65
- ),
195
+ ) : null,
66
196
  h(
67
197
  "div",
68
- { className: "p-6", style: theme.card },
69
- h("h3", { className: "text-lg font-semibold mb-3", style: theme.text }, "Connected Accounts"),
70
- h(
198
+ { style: t.card },
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(
71
201
  "div",
72
- { className: "text-center py-8" },
73
- h("div", { className: "text-4xl mb-3 opacity-60" }, "\u{1F50C}"),
74
- h("p", { className: "text-sm", style: theme.textMuted }, "No accounts connected yet"),
75
- h("p", { className: "text-xs mt-1", style: theme.textFaint }, "Click a platform above to get started")
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
+ )
76
220
  )
77
221
  )
78
222
  );
@@ -4,32 +4,17 @@
4
4
  const R = window.React;
5
5
  if (!R) return;
6
6
  const h = R.createElement;
7
- const theme = {
7
+ const t = {
8
8
  text: { color: "var(--ck-text-primary)" },
9
- textMuted: { color: "var(--ck-text-secondary)" },
10
- textFaint: { color: "var(--ck-text-tertiary)" },
9
+ muted: { color: "var(--ck-text-secondary)" },
10
+ faint: { color: "var(--ck-text-tertiary)" },
11
11
  card: {
12
- background: "var(--ck-bg-glass)",
12
+ background: "rgba(255,255,255,0.03)",
13
13
  border: "1px solid var(--ck-border-subtle)",
14
- borderRadius: "14px",
15
- backdropFilter: "blur(18px) saturate(1.25)"
16
- },
17
- banner: {
18
- background: "var(--ck-bg-glass)",
19
- border: "1px solid rgba(183,148,244,0.25)",
20
- borderRadius: "14px",
21
- color: "rgba(183,148,244,0.9)",
22
- backdropFilter: "blur(18px)"
23
- },
24
- statLabel: { color: "var(--ck-text-tertiary)", fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "0.05em" },
25
- chartPlaceholder: {
26
- border: "1px dashed var(--ck-border-subtle)",
27
14
  borderRadius: "10px",
28
- height: "16rem",
29
- display: "flex",
30
- alignItems: "center",
31
- justifyContent: "center"
32
- }
15
+ padding: "1rem"
16
+ },
17
+ statLabel: { color: "var(--ck-text-tertiary)", fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "0.05em" }
33
18
  };
34
19
  const stats = [
35
20
  { label: "Total Posts", value: "0", color: "rgba(99,179,237,0.9)" },
@@ -39,36 +24,27 @@
39
24
  function Analytics() {
40
25
  return h(
41
26
  "div",
42
- { className: "p-8 max-w-4xl mx-auto" },
43
- h("h2", { className: "text-2xl font-bold mb-6", style: theme.text }, "Analytics"),
44
- h("div", { className: "p-4 mb-6 text-sm", style: theme.banner }, "\u{1F4CA} Track your content performance across platforms"),
27
+ { className: "space-y-3" },
45
28
  h(
46
29
  "div",
47
- { className: "grid grid-cols-3 gap-4 mb-6" },
30
+ { className: "grid grid-cols-3 gap-3" },
48
31
  ...stats.map(
49
32
  (s) => h(
50
33
  "div",
51
- { key: s.label, className: "p-5 text-center", style: theme.card },
52
- h("div", { className: "text-3xl font-bold mb-1", style: { color: s.color } }, s.value),
53
- h("div", { style: theme.statLabel }, s.label)
34
+ { key: s.label, className: "text-center p-3", style: t.card },
35
+ h("div", { className: "text-2xl font-bold mb-1", style: { color: s.color } }, s.value),
36
+ h("div", { style: t.statLabel }, s.label)
54
37
  )
55
38
  )
56
39
  ),
57
40
  h(
58
41
  "div",
59
- { className: "p-6", style: theme.card },
60
- h("h3", { className: "text-lg font-semibold mb-4", style: theme.text }, "Engagement Over Time"),
61
- h(
62
- "div",
63
- { style: theme.chartPlaceholder },
64
- h(
65
- "div",
66
- { className: "text-center" },
67
- h("div", { className: "text-4xl mb-3 opacity-60" }, "\u{1F4C8}"),
68
- h("p", { className: "text-sm", style: theme.textMuted }, "Your engagement chart will appear here"),
69
- h("p", { className: "text-xs mt-1", style: theme.textFaint }, "Start publishing content to see analytics")
70
- )
71
- )
42
+ { style: t.card },
43
+ h("div", { className: "text-sm font-medium mb-3", style: t.text }, "Engagement Over Time"),
44
+ h("div", {
45
+ className: "flex items-center justify-center py-8 text-sm",
46
+ style: { ...t.faint, border: "1px dashed var(--ck-border-subtle)", borderRadius: "8px" }
47
+ }, "Start publishing content to see analytics")
72
48
  )
73
49
  );
74
50
  }
@@ -4,50 +4,34 @@
4
4
  const R = window.React;
5
5
  if (!R) return;
6
6
  const h = R.createElement;
7
- const theme = {
7
+ const t = {
8
8
  text: { color: "var(--ck-text-primary)" },
9
- textMuted: { color: "var(--ck-text-secondary)" },
10
- textFaint: { color: "var(--ck-text-tertiary)" },
9
+ muted: { color: "var(--ck-text-secondary)" },
10
+ faint: { color: "var(--ck-text-tertiary)" },
11
11
  card: {
12
- background: "var(--ck-bg-glass)",
12
+ background: "rgba(255,255,255,0.03)",
13
13
  border: "1px solid var(--ck-border-subtle)",
14
- borderRadius: "14px",
15
- backdropFilter: "blur(18px) saturate(1.25)"
16
- },
17
- banner: {
18
- background: "var(--ck-bg-glass)",
19
- border: "1px solid rgba(72,187,120,0.25)",
20
- borderRadius: "14px",
21
- color: "rgba(72,187,120,0.9)",
22
- backdropFilter: "blur(18px)"
23
- },
24
- dayHeader: { color: "var(--ck-text-tertiary)", fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "0.05em" },
25
- cell: {
26
- border: "1px solid var(--ck-border-subtle)",
27
- borderRadius: "8px",
28
- minHeight: "5rem",
29
- background: "rgba(255,255,255,0.02)"
30
- },
31
- cellEmpty: {
32
- border: "1px solid transparent",
33
- borderRadius: "8px",
34
- minHeight: "5rem",
35
- opacity: 0.2
14
+ borderRadius: "10px",
15
+ padding: "1rem"
36
16
  },
17
+ dayHeader: { color: "var(--ck-text-tertiary)", fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "0.05em", fontWeight: 600 },
18
+ cell: { border: "1px solid var(--ck-border-subtle)", borderRadius: "6px", minHeight: "4rem", padding: "0.35rem 0.5rem" },
19
+ cellEmpty: { minHeight: "4rem", opacity: 0.15 },
37
20
  dayNum: { color: "var(--ck-text-secondary)", fontSize: "0.8rem", fontWeight: 500 },
38
21
  todayBadge: {
39
22
  color: "rgba(99,179,237,1)",
40
23
  background: "rgba(99,179,237,0.15)",
41
24
  fontSize: "0.8rem",
42
25
  fontWeight: 700,
43
- width: "1.6rem",
44
- height: "1.6rem",
26
+ width: "1.5rem",
27
+ height: "1.5rem",
45
28
  borderRadius: "50%",
46
29
  display: "inline-flex",
47
30
  alignItems: "center",
48
31
  justifyContent: "center"
49
32
  }
50
33
  };
34
+ const grid7 = { display: "grid", gridTemplateColumns: "repeat(7,1fr)", gap: "3px" };
51
35
  const now = /* @__PURE__ */ new Date();
52
36
  const year = now.getFullYear();
53
37
  const month = now.getMonth();
@@ -61,35 +45,29 @@
61
45
  for (let i = 0; i < 42; i++) {
62
46
  const day = i - firstDay + 1;
63
47
  if (day < 1 || day > daysInMonth) {
64
- cells.push(h("div", { key: i, style: theme.cellEmpty }));
48
+ cells.push(h("div", { key: i, style: t.cellEmpty }));
65
49
  } else {
66
50
  const isToday = day === today;
67
51
  cells.push(
68
52
  h(
69
53
  "div",
70
- { key: i, className: "p-2", style: theme.cell },
71
- h("span", { style: isToday ? theme.todayBadge : theme.dayNum }, day)
54
+ { key: i, style: t.cell },
55
+ h("span", { style: isToday ? t.todayBadge : t.dayNum }, day)
72
56
  )
73
57
  );
74
58
  }
75
59
  }
76
60
  return h(
77
61
  "div",
78
- { className: "p-8 max-w-4xl mx-auto" },
79
- h("h2", { className: "text-2xl font-bold mb-6", style: theme.text }, "Content Calendar"),
80
- h("div", { className: "p-4 mb-6 text-sm", style: theme.banner }, "\u{1F4C5} Schedule and plan your content"),
62
+ { style: t.card },
63
+ h("div", { className: "text-sm font-medium mb-3", style: t.text }, monthName),
81
64
  h(
82
65
  "div",
83
- { className: "p-6", style: theme.card },
84
- h("div", { className: "text-center font-semibold text-lg mb-4", style: theme.text }, monthName),
85
- h(
86
- "div",
87
- { style: { display: "grid", gridTemplateColumns: "repeat(7,1fr)", gap: "4px", marginBottom: "4px" } },
88
- ...dayNames.map((d) => h("div", { key: d, className: "text-center py-2 font-semibold", style: theme.dayHeader }, d))
89
- ),
90
- h("div", { style: { display: "grid", gridTemplateColumns: "repeat(7,1fr)", gap: "4px" } }, ...cells),
91
- h("p", { className: "text-xs mt-4", style: theme.textFaint }, "Scheduled posts will appear on their respective dates.")
92
- )
66
+ { style: { ...grid7, marginBottom: "3px" } },
67
+ ...dayNames.map((d) => h("div", { key: d, className: "text-center py-1", style: t.dayHeader }, d))
68
+ ),
69
+ h("div", { style: grid7 }, ...cells),
70
+ h("div", { className: "mt-3 text-xs", style: t.faint }, "Scheduled posts will appear on their respective dates.")
93
71
  );
94
72
  }
95
73
  window.KitchenPlugin.registerTab("marketing", "content-calendar", ContentCalendar);
@@ -4,57 +4,218 @@
4
4
  const R = window.React;
5
5
  if (!R) return;
6
6
  const h = R.createElement;
7
- const theme = {
7
+ const useEffect = R.useEffect;
8
+ const useMemo = R.useMemo;
9
+ const useState = R.useState;
10
+ const t = {
8
11
  text: { color: "var(--ck-text-primary)" },
9
- textMuted: { color: "var(--ck-text-secondary)" },
10
- textFaint: { color: "var(--ck-text-tertiary)" },
12
+ muted: { color: "var(--ck-text-secondary)" },
13
+ faint: { color: "var(--ck-text-tertiary)" },
11
14
  card: {
12
- background: "var(--ck-bg-glass)",
15
+ background: "rgba(255,255,255,0.03)",
13
16
  border: "1px solid var(--ck-border-subtle)",
14
- borderRadius: "14px",
15
- backdropFilter: "blur(18px) saturate(1.25)"
17
+ borderRadius: "10px",
18
+ padding: "1rem"
16
19
  },
17
- banner: {
18
- background: "var(--ck-bg-glass)",
19
- border: "1px solid rgba(99,179,237,0.25)",
20
- borderRadius: "14px",
21
- color: "rgba(99,179,237,0.9)",
22
- backdropFilter: "blur(18px)"
23
- }
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
+ })
24
56
  };
25
- 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
+ };
26
120
  return h(
27
121
  "div",
28
- { className: "p-8 max-w-4xl mx-auto" },
29
- h("h2", { className: "text-2xl font-bold mb-6", style: theme.text }, "Content Library"),
30
- h("div", { className: "p-4 mb-6 text-sm", style: theme.banner }, "\u{1F389} Marketing Suite plugin is active! Manage your content from here."),
122
+ { className: "space-y-3" },
31
123
  h(
32
124
  "div",
33
- { className: "flex flex-col gap-4" },
125
+ { style: t.card },
126
+ h("div", { className: "text-sm font-medium mb-3", style: t.text }, "New Post"),
34
127
  h(
35
128
  "div",
36
- { className: "p-6", style: theme.card },
37
- h("h3", { className: "text-lg font-semibold mb-3", style: theme.text }, "Create New Post"),
38
- h("p", { className: "text-sm mb-2", style: theme.textMuted }, "Your content creation tools will live here:"),
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
+ }),
39
137
  h(
40
- "ul",
41
- { className: "text-sm space-y-1", style: { ...theme.textMuted, listStyle: "disc inside", paddingLeft: "0.5rem" } },
42
- h("li", null, "Rich text editor with media embedding"),
43
- h("li", null, "Multi-platform publishing (Twitter, Instagram, LinkedIn)"),
44
- h("li", null, "Scheduling & auto-posting"),
45
- h("li", null, "Template library for quick starts")
46
- )
47
- ),
48
- h(
49
- "div",
50
- { className: "p-6", style: theme.card },
51
- h("h3", { className: "text-lg font-semibold mb-3", style: theme.text }, "Recent Posts"),
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
+ ),
52
172
  h(
53
173
  "div",
54
- { className: "text-center py-8" },
55
- h("div", { className: "text-4xl mb-3 opacity-60" }, "\u270D\uFE0F"),
56
- h("p", { className: "text-sm", style: theme.textMuted }, "No posts yet"),
57
- h("p", { className: "text-xs mt-1", style: theme.textFaint }, "Create your first post to get started")
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
193
+ )
194
+ ),
195
+ h(
196
+ "div",
197
+ { style: t.card },
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
+ )
58
219
  )
59
220
  )
60
221
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiggai/kitchen-plugin-marketing",
3
- "version": "0.2.4",
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/routes.js",
48
+ "apiRoutes": "./dist/api/handler.js",
49
49
  "migrations": "./db/migrations"
50
50
  },
51
51
  "scripts": {