@jiggai/kitchen-plugin-marketing 0.3.4 → 0.4.0

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.
@@ -954,6 +954,54 @@ async function handleRequest(req, ctx) {
954
954
  return apiError(500, "DATABASE_ERROR", error?.message || "Unknown error");
955
955
  }
956
956
  }
957
+ const singlePostMatch = req.path.match(/^\/posts\/([a-f0-9-]+)$/);
958
+ if (singlePostMatch && req.method === "GET") {
959
+ try {
960
+ const { db } = initializeDatabase(teamId);
961
+ const [post] = await db.select().from(posts).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(posts.id, singlePostMatch[1]), (0, import_drizzle_orm2.eq)(posts.teamId, teamId)));
962
+ if (!post) return apiError(404, "NOT_FOUND", "Post not found");
963
+ return {
964
+ status: 200,
965
+ data: {
966
+ ...post,
967
+ platforms: JSON.parse(post.platforms || "[]"),
968
+ tags: JSON.parse(post.tags || "[]"),
969
+ mediaIds: JSON.parse(post.mediaIds || "[]")
970
+ }
971
+ };
972
+ } catch (error) {
973
+ return apiError(500, "DATABASE_ERROR", error?.message || "Unknown error");
974
+ }
975
+ }
976
+ if (singlePostMatch && req.method === "DELETE") {
977
+ try {
978
+ const { db } = initializeDatabase(teamId);
979
+ const [post] = await db.select().from(posts).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(posts.id, singlePostMatch[1]), (0, import_drizzle_orm2.eq)(posts.teamId, teamId)));
980
+ if (!post) return apiError(404, "NOT_FOUND", "Post not found");
981
+ await db.delete(posts).where((0, import_drizzle_orm2.eq)(posts.id, singlePostMatch[1]));
982
+ return { status: 200, data: { deleted: true, id: singlePostMatch[1] } };
983
+ } catch (error) {
984
+ return apiError(500, "DATABASE_ERROR", error?.message || "Unknown error");
985
+ }
986
+ }
987
+ if (singlePostMatch && req.method === "PATCH") {
988
+ try {
989
+ const { db } = initializeDatabase(teamId);
990
+ const [post] = await db.select().from(posts).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(posts.id, singlePostMatch[1]), (0, import_drizzle_orm2.eq)(posts.teamId, teamId)));
991
+ if (!post) return apiError(404, "NOT_FOUND", "Post not found");
992
+ const body = req.body || {};
993
+ const updates = { updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
994
+ if (body.content !== void 0) updates.content = body.content;
995
+ if (body.platforms !== void 0) updates.platforms = JSON.stringify(body.platforms);
996
+ if (body.status !== void 0) updates.status = body.status;
997
+ if (body.scheduledAt !== void 0) updates.scheduledAt = body.scheduledAt || null;
998
+ if (body.tags !== void 0) updates.tags = JSON.stringify(body.tags);
999
+ await db.update(posts).set(updates).where((0, import_drizzle_orm2.eq)(posts.id, singlePostMatch[1]));
1000
+ return { status: 200, data: { updated: true, id: singlePostMatch[1] } };
1001
+ } catch (error) {
1002
+ return apiError(500, "DATABASE_ERROR", error?.message || "Unknown error");
1003
+ }
1004
+ }
957
1005
  return apiError(501, "NOT_IMPLEMENTED", `No handler for ${req.method} ${req.path}`);
958
1006
  }
959
1007
  // Annotate the CommonJS export names for ESM import in node:
@@ -4,70 +4,915 @@
4
4
  const R = window.React;
5
5
  if (!R) return;
6
6
  const h = R.createElement;
7
- const t = {
8
- text: { color: "var(--ck-text-primary)" },
9
- muted: { color: "var(--ck-text-secondary)" },
10
- faint: { color: "var(--ck-text-tertiary)" },
11
- card: {
12
- background: "rgba(255,255,255,0.03)",
7
+ const useState = R.useState;
8
+ const useEffect = R.useEffect;
9
+ const useMemo = R.useMemo;
10
+ const useCallback = R.useCallback;
11
+ const useRef = R.useRef;
12
+ const s = {
13
+ // layout
14
+ container: { color: "var(--ck-text-primary)" },
15
+ // top bar
16
+ topBar: {
17
+ display: "flex",
18
+ alignItems: "center",
19
+ justifyContent: "space-between",
20
+ marginBottom: "1rem",
21
+ flexWrap: "wrap",
22
+ gap: "0.5rem"
23
+ },
24
+ navGroup: { display: "flex", alignItems: "center", gap: "0.5rem" },
25
+ navBtn: {
26
+ background: "rgba(255,255,255,0.06)",
27
+ border: "1px solid var(--ck-border-subtle)",
28
+ borderRadius: "8px",
29
+ padding: "0.35rem 0.65rem",
30
+ color: "var(--ck-text-primary)",
31
+ cursor: "pointer",
32
+ fontWeight: 600,
33
+ fontSize: "0.85rem"
34
+ },
35
+ navBtnActive: {
36
+ background: "rgba(127,90,240,0.25)",
37
+ border: "1px solid rgba(127,90,240,0.5)",
38
+ borderRadius: "8px",
39
+ padding: "0.35rem 0.65rem",
40
+ color: "white",
41
+ cursor: "pointer",
42
+ fontWeight: 600,
43
+ fontSize: "0.85rem"
44
+ },
45
+ dateLabel: { color: "var(--ck-text-primary)", fontWeight: 600, fontSize: "0.9rem", minWidth: "200px", textAlign: "center" },
46
+ // Week grid
47
+ weekGrid: { display: "grid", gridTemplateColumns: "60px repeat(7, 1fr)", gap: "0" },
48
+ dayColHeader: {
49
+ textAlign: "center",
50
+ padding: "0.6rem 0.25rem",
51
+ fontSize: "0.8rem",
52
+ color: "var(--ck-text-tertiary)",
53
+ fontWeight: 600,
54
+ borderBottom: "1px solid var(--ck-border-subtle)"
55
+ },
56
+ dayColHeaderToday: {
57
+ textAlign: "center",
58
+ padding: "0.6rem 0.25rem",
59
+ fontSize: "0.8rem",
60
+ color: "rgba(248,113,113,1)",
61
+ fontWeight: 700,
62
+ borderBottom: "1px solid var(--ck-border-subtle)"
63
+ },
64
+ dayDate: { fontSize: "0.85rem", fontWeight: 600, color: "var(--ck-text-secondary)" },
65
+ dayDateToday: { fontSize: "0.85rem", fontWeight: 700, color: "rgba(248,113,113,1)" },
66
+ timeLabel: {
67
+ fontSize: "0.75rem",
68
+ color: "var(--ck-text-tertiary)",
69
+ textAlign: "right",
70
+ paddingRight: "0.5rem",
71
+ paddingTop: "0.15rem",
72
+ borderRight: "1px solid var(--ck-border-subtle)"
73
+ },
74
+ timeSlot: {
75
+ borderBottom: "1px solid rgba(255,255,255,0.04)",
76
+ borderRight: "1px solid rgba(255,255,255,0.04)",
77
+ minHeight: "60px",
78
+ position: "relative",
79
+ padding: "2px"
80
+ },
81
+ // Month grid
82
+ monthGrid: { display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: "3px" },
83
+ monthDayHeader: {
84
+ textAlign: "center",
85
+ padding: "0.4rem",
86
+ fontSize: "0.75rem",
87
+ color: "var(--ck-text-tertiary)",
88
+ fontWeight: 600,
89
+ textTransform: "uppercase"
90
+ },
91
+ monthCell: {
13
92
  border: "1px solid var(--ck-border-subtle)",
14
93
  borderRadius: "10px",
15
- padding: "1rem"
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 },
20
- dayNum: { color: "var(--ck-text-secondary)", fontSize: "0.8rem", fontWeight: 500 },
21
- todayBadge: {
22
- color: "rgba(99,179,237,1)",
23
- background: "rgba(99,179,237,0.15)",
94
+ minHeight: "90px",
95
+ padding: "0.4rem",
96
+ position: "relative",
97
+ background: "rgba(255,255,255,0.015)"
98
+ },
99
+ monthCellEmpty: { minHeight: "90px", opacity: 0.1 },
100
+ monthDayNum: { fontSize: "0.8rem", fontWeight: 500, color: "var(--ck-text-secondary)", marginBottom: "0.25rem" },
101
+ monthDayNumToday: {
24
102
  fontSize: "0.8rem",
25
103
  fontWeight: 700,
26
- width: "1.5rem",
27
- height: "1.5rem",
104
+ color: "rgba(248,113,113,1)",
105
+ background: "rgba(248,113,113,0.12)",
28
106
  borderRadius: "50%",
107
+ width: "1.6rem",
108
+ height: "1.6rem",
29
109
  display: "inline-flex",
30
110
  alignItems: "center",
31
- justifyContent: "center"
111
+ justifyContent: "center",
112
+ marginBottom: "0.25rem"
113
+ },
114
+ // Plus button
115
+ plusBtn: {
116
+ position: "absolute",
117
+ bottom: "6px",
118
+ right: "6px",
119
+ background: "rgba(127,90,240,0.85)",
120
+ border: "none",
121
+ borderRadius: "8px",
122
+ width: "28px",
123
+ height: "28px",
124
+ display: "flex",
125
+ alignItems: "center",
126
+ justifyContent: "center",
127
+ color: "white",
128
+ fontSize: "1.1rem",
129
+ fontWeight: 700,
130
+ cursor: "pointer",
131
+ opacity: 0,
132
+ transition: "opacity 0.15s"
133
+ },
134
+ plusBtnVisible: { opacity: 1 },
135
+ // Post card on calendar
136
+ postCard: {
137
+ background: "rgba(127,90,240,0.55)",
138
+ borderRadius: "6px",
139
+ padding: "3px 6px",
140
+ fontSize: "0.7rem",
141
+ color: "white",
142
+ marginBottom: "2px",
143
+ cursor: "pointer",
144
+ overflow: "hidden",
145
+ whiteSpace: "nowrap",
146
+ textOverflow: "ellipsis",
147
+ position: "relative"
148
+ },
149
+ postCardFailed: { borderLeft: "3px solid rgba(248,113,113,0.9)" },
150
+ postCardPublished: { borderLeft: "3px solid rgba(74,222,128,0.9)" },
151
+ postCardDraft: { borderLeft: "3px solid rgba(167,139,250,0.7)" },
152
+ postCardScheduled: { borderLeft: "3px solid rgba(251,191,36,0.7)" },
153
+ // Post card hover actions
154
+ cardActions: {
155
+ position: "absolute",
156
+ top: "-1px",
157
+ right: "0",
158
+ display: "flex",
159
+ gap: "2px",
160
+ background: "rgba(127,90,240,0.95)",
161
+ borderRadius: "0 6px 6px 0",
162
+ padding: "2px 4px"
163
+ },
164
+ cardActionBtn: {
165
+ background: "none",
166
+ border: "none",
167
+ color: "white",
168
+ cursor: "pointer",
169
+ fontSize: "0.75rem",
170
+ padding: "1px 3px",
171
+ borderRadius: "3px"
172
+ },
173
+ // Modal
174
+ overlay: {
175
+ position: "fixed",
176
+ inset: "0",
177
+ background: "rgba(0,0,0,0.65)",
178
+ display: "flex",
179
+ alignItems: "center",
180
+ justifyContent: "center",
181
+ zIndex: 9999,
182
+ backdropFilter: "blur(4px)"
183
+ },
184
+ modal: {
185
+ background: "var(--ck-bg-base, #0b0c10)",
186
+ border: "1px solid var(--ck-border-subtle)",
187
+ borderRadius: "14px",
188
+ width: "95vw",
189
+ maxWidth: "900px",
190
+ maxHeight: "90vh",
191
+ overflow: "auto",
192
+ display: "flex",
193
+ flexDirection: "column"
194
+ },
195
+ modalHeader: {
196
+ display: "flex",
197
+ alignItems: "center",
198
+ justifyContent: "space-between",
199
+ padding: "1rem 1.25rem",
200
+ borderBottom: "1px solid var(--ck-border-subtle)"
201
+ },
202
+ modalBody: { display: "flex", flex: 1, minHeight: 0 },
203
+ modalLeft: { flex: 1, padding: "1rem 1.25rem", borderRight: "1px solid var(--ck-border-subtle)", display: "flex", flexDirection: "column", gap: "0.75rem" },
204
+ modalRight: { width: "280px", padding: "1rem 1.25rem", flexShrink: 0 },
205
+ modalFooter: {
206
+ display: "flex",
207
+ alignItems: "center",
208
+ justifyContent: "space-between",
209
+ padding: "0.75rem 1.25rem",
210
+ borderTop: "1px solid var(--ck-border-subtle)",
211
+ flexWrap: "wrap",
212
+ gap: "0.5rem"
213
+ },
214
+ closeBtn: {
215
+ background: "none",
216
+ border: "none",
217
+ color: "var(--ck-text-tertiary)",
218
+ cursor: "pointer",
219
+ fontSize: "1.4rem",
220
+ padding: "0.25rem"
221
+ },
222
+ textarea: {
223
+ background: "rgba(255,255,255,0.03)",
224
+ border: "1px solid var(--ck-border-subtle)",
225
+ borderRadius: "10px",
226
+ padding: "0.75rem",
227
+ color: "var(--ck-text-primary)",
228
+ width: "100%",
229
+ minHeight: "200px",
230
+ resize: "vertical",
231
+ fontFamily: "inherit",
232
+ fontSize: "0.9rem",
233
+ outline: "none"
234
+ },
235
+ input: {
236
+ background: "rgba(255,255,255,0.03)",
237
+ border: "1px solid var(--ck-border-subtle)",
238
+ borderRadius: "10px",
239
+ padding: "0.5rem 0.75rem",
240
+ color: "var(--ck-text-primary)",
241
+ width: "100%",
242
+ fontSize: "0.85rem",
243
+ outline: "none"
244
+ },
245
+ platformCircle: (active, connected) => ({
246
+ width: "36px",
247
+ height: "36px",
248
+ borderRadius: "50%",
249
+ display: "flex",
250
+ alignItems: "center",
251
+ justifyContent: "center",
252
+ fontSize: "1.1rem",
253
+ cursor: connected ? "pointer" : "default",
254
+ border: active ? "2px solid rgba(127,90,240,0.8)" : "2px solid var(--ck-border-subtle)",
255
+ background: active ? "rgba(127,90,240,0.2)" : "rgba(255,255,255,0.03)",
256
+ opacity: connected ? 1 : 0.35,
257
+ transition: "all 0.15s"
258
+ }),
259
+ btnPrimary: {
260
+ background: "rgba(127,90,240,0.85)",
261
+ border: "none",
262
+ borderRadius: "10px",
263
+ padding: "0.55rem 1rem",
264
+ color: "white",
265
+ fontWeight: 700,
266
+ cursor: "pointer",
267
+ fontSize: "0.85rem"
268
+ },
269
+ btnGhost: {
270
+ background: "rgba(255,255,255,0.05)",
271
+ border: "1px solid var(--ck-border-subtle)",
272
+ borderRadius: "10px",
273
+ padding: "0.55rem 1rem",
274
+ color: "var(--ck-text-primary)",
275
+ fontWeight: 600,
276
+ cursor: "pointer",
277
+ fontSize: "0.85rem"
278
+ },
279
+ statusDot: (status) => {
280
+ const c = {
281
+ draft: "rgba(167,139,250,0.8)",
282
+ scheduled: "rgba(251,191,36,0.8)",
283
+ published: "rgba(74,222,128,0.8)",
284
+ failed: "rgba(248,113,113,0.9)"
285
+ };
286
+ return {
287
+ width: "8px",
288
+ height: "8px",
289
+ borderRadius: "50%",
290
+ background: c[status] || "rgba(100,100,100,0.5)",
291
+ flexShrink: 0
292
+ };
293
+ },
294
+ previewPanel: {
295
+ background: "rgba(255,255,255,0.02)",
296
+ border: "1px solid var(--ck-border-subtle)",
297
+ borderRadius: "10px",
298
+ padding: "0.75rem",
299
+ minHeight: "120px"
32
300
  }
33
301
  };
34
- const grid7 = { display: "grid", gridTemplateColumns: "repeat(7,1fr)", gap: "3px" };
35
- const now = /* @__PURE__ */ new Date();
36
- const year = now.getFullYear();
37
- const month = now.getMonth();
38
- const today = now.getDate();
39
- const firstDay = new Date(year, month, 1).getDay();
40
- const daysInMonth = new Date(year, month + 1, 0).getDate();
41
- const monthName = now.toLocaleString("default", { month: "long", year: "numeric" });
42
- const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
43
- function ContentCalendar() {
44
- const cells = [];
45
- for (let i = 0; i < 42; i++) {
46
- const day = i - firstDay + 1;
47
- if (day < 1 || day > daysInMonth) {
48
- cells.push(h("div", { key: i, style: t.cellEmpty }));
49
- } else {
50
- const isToday = day === today;
51
- cells.push(
302
+ const DAY_NAMES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
303
+ const DAY_NAMES_SHORT = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
304
+ const MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
305
+ function startOfWeek(d) {
306
+ const clone = new Date(d);
307
+ const day = clone.getDay();
308
+ const diff = day === 0 ? -6 : 1 - day;
309
+ clone.setDate(clone.getDate() + diff);
310
+ clone.setHours(0, 0, 0, 0);
311
+ return clone;
312
+ }
313
+ function addDays(d, n) {
314
+ const c = new Date(d);
315
+ c.setDate(c.getDate() + n);
316
+ return c;
317
+ }
318
+ function isSameDay(a, b) {
319
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
320
+ }
321
+ function fmt(d) {
322
+ return `${String(d.getMonth() + 1).padStart(2, "0")}/${String(d.getDate()).padStart(2, "0")}/${d.getFullYear()}`;
323
+ }
324
+ function isFutureOrToday(d) {
325
+ const today = /* @__PURE__ */ new Date();
326
+ today.setHours(0, 0, 0, 0);
327
+ return d >= today;
328
+ }
329
+ function ContentCalendar(props) {
330
+ const teamId = String(props?.teamId || "default");
331
+ const apiBase = "/api/plugins/marketing";
332
+ const today = /* @__PURE__ */ new Date();
333
+ const [view, setView] = useState("week");
334
+ const [anchor, setAnchor] = useState(() => startOfWeek(today));
335
+ const [posts, setPosts] = useState([]);
336
+ const [drivers, setDrivers] = useState([]);
337
+ const [loading, setLoading] = useState(true);
338
+ const [hoverDay, setHoverDay] = useState(null);
339
+ const [hoverPost, setHoverPost] = useState(null);
340
+ const [modalOpen, setModalOpen] = useState(false);
341
+ const [modalDate, setModalDate] = useState("");
342
+ const [modalContent, setModalContent] = useState("");
343
+ const [modalPlatforms, setModalPlatforms] = useState([]);
344
+ const [modalMediaUrl, setModalMediaUrl] = useState("");
345
+ const [modalSaving, setModalSaving] = useState(false);
346
+ const [modalPublishing, setModalPublishing] = useState(false);
347
+ const [modalError, setModalError] = useState(null);
348
+ const [modalSuccess, setModalSuccess] = useState(null);
349
+ const [previewPost, setPreviewPost] = useState(null);
350
+ const postizHeaders = useMemo(() => {
351
+ try {
352
+ const stored = localStorage.getItem(`ck-postiz-${teamId}`);
353
+ if (stored) {
354
+ const parsed = JSON.parse(stored);
355
+ if (parsed.apiKey) return { "x-postiz-api-key": parsed.apiKey, "x-postiz-base-url": parsed.baseUrl || "https://api.postiz.com/public/v1" };
356
+ }
357
+ } catch {
358
+ }
359
+ return {};
360
+ }, [teamId]);
361
+ const loadPosts = useCallback(async () => {
362
+ try {
363
+ const res = await fetch(`${apiBase}/posts?team=${encodeURIComponent(teamId)}&limit=200`);
364
+ const json = await res.json();
365
+ setPosts(Array.isArray(json.data) ? json.data : []);
366
+ } catch {
367
+ }
368
+ }, [teamId]);
369
+ const loadDrivers = useCallback(async () => {
370
+ try {
371
+ const res = await fetch(`${apiBase}/drivers?team=${encodeURIComponent(teamId)}`, { headers: postizHeaders });
372
+ const json = await res.json();
373
+ setDrivers(Array.isArray(json.drivers) ? json.drivers : []);
374
+ } catch {
375
+ }
376
+ }, [teamId, postizHeaders]);
377
+ useEffect(() => {
378
+ setLoading(true);
379
+ Promise.all([loadPosts(), loadDrivers()]).finally(() => setLoading(false));
380
+ }, [loadPosts, loadDrivers]);
381
+ const goToday = () => setAnchor(startOfWeek(today));
382
+ const goPrev = () => setAnchor(view === "week" ? addDays(anchor, -7) : new Date(anchor.getFullYear(), anchor.getMonth() - 1, 1));
383
+ const goNext = () => setAnchor(view === "week" ? addDays(anchor, 7) : new Date(anchor.getFullYear(), anchor.getMonth() + 1, 1));
384
+ const postsForDay = useCallback((day) => {
385
+ return posts.filter((p) => {
386
+ const d = p.scheduledAt ? new Date(p.scheduledAt) : new Date(p.createdAt);
387
+ return isSameDay(d, day);
388
+ });
389
+ }, [posts]);
390
+ const openCreateModal = (dateStr) => {
391
+ const d = dateStr || new Date(today.getFullYear(), today.getMonth(), today.getDate(), 17, 0).toISOString().slice(0, 16);
392
+ setModalDate(d);
393
+ setModalContent("");
394
+ setModalPlatforms([]);
395
+ setModalMediaUrl("");
396
+ setModalError(null);
397
+ setModalSuccess(null);
398
+ setModalOpen(true);
399
+ };
400
+ const deletePost = async (id) => {
401
+ try {
402
+ await fetch(`${apiBase}/posts/${id}?team=${encodeURIComponent(teamId)}`, { method: "DELETE" });
403
+ setPosts((prev) => prev.filter((p) => p.id !== id));
404
+ if (previewPost?.id === id) setPreviewPost(null);
405
+ } catch {
406
+ }
407
+ };
408
+ const copyPost = (content) => {
409
+ try {
410
+ navigator.clipboard.writeText(content);
411
+ } catch {
412
+ }
413
+ };
414
+ const modalSaveDraft = async () => {
415
+ if (!modalContent.trim()) return;
416
+ setModalSaving(true);
417
+ setModalError(null);
418
+ try {
419
+ const res = await fetch(`${apiBase}/posts?team=${encodeURIComponent(teamId)}`, {
420
+ method: "POST",
421
+ headers: { "content-type": "application/json" },
422
+ body: JSON.stringify({
423
+ content: modalContent,
424
+ platforms: modalPlatforms.length > 0 ? modalPlatforms : ["draft"],
425
+ status: modalDate ? "scheduled" : "draft",
426
+ scheduledAt: modalDate || void 0
427
+ })
428
+ });
429
+ if (!res.ok) throw new Error(`Save failed (${res.status})`);
430
+ setModalSuccess("Saved!");
431
+ await loadPosts();
432
+ setTimeout(() => {
433
+ setModalOpen(false);
434
+ setModalSuccess(null);
435
+ }, 800);
436
+ } catch (e) {
437
+ setModalError(e?.message || "Failed to save");
438
+ } finally {
439
+ setModalSaving(false);
440
+ }
441
+ };
442
+ const modalPublish = async () => {
443
+ if (!modalContent.trim() || modalPlatforms.length === 0) return;
444
+ setModalPublishing(true);
445
+ setModalError(null);
446
+ try {
447
+ const res = await fetch(`${apiBase}/publish?team=${encodeURIComponent(teamId)}`, {
448
+ method: "POST",
449
+ headers: { "content-type": "application/json", ...postizHeaders },
450
+ body: JSON.stringify({
451
+ content: modalContent,
452
+ platforms: modalPlatforms,
453
+ scheduledAt: modalDate || void 0,
454
+ mediaUrls: modalMediaUrl ? [modalMediaUrl] : void 0
455
+ })
456
+ });
457
+ const json = await res.json();
458
+ if (json.results) {
459
+ const failed = json.results.filter((r) => !r.success);
460
+ if (failed.length > 0 && json.results.every((r) => !r.success)) {
461
+ throw new Error(failed.map((f) => `${f.platform}: ${f.error}`).join("; "));
462
+ }
463
+ }
464
+ await fetch(`${apiBase}/posts?team=${encodeURIComponent(teamId)}`, {
465
+ method: "POST",
466
+ headers: { "content-type": "application/json" },
467
+ body: JSON.stringify({
468
+ content: modalContent,
469
+ platforms: modalPlatforms,
470
+ status: modalDate ? "scheduled" : "published",
471
+ scheduledAt: modalDate || void 0
472
+ })
473
+ }).catch(() => {
474
+ });
475
+ setModalSuccess(modalDate ? "Scheduled!" : "Published!");
476
+ await loadPosts();
477
+ setTimeout(() => {
478
+ setModalOpen(false);
479
+ setModalSuccess(null);
480
+ }, 1e3);
481
+ } catch (e) {
482
+ setModalError(e?.message || "Publish failed");
483
+ } finally {
484
+ setModalPublishing(false);
485
+ }
486
+ };
487
+ const connectedDrivers = useMemo(() => drivers.filter((d) => d.connected), [drivers]);
488
+ function PostCard({ post, compact }) {
489
+ const isHover = hoverPost === post.id;
490
+ const statusBorder = s["postCard" + post.status.charAt(0).toUpperCase() + post.status.slice(1)] || {};
491
+ const driver = post.platforms?.[0] ? drivers.find((d) => d.platform === post.platforms[0]) : null;
492
+ return h(
493
+ "div",
494
+ {
495
+ style: { ...s.postCard, ...statusBorder, ...compact ? {} : { whiteSpace: "normal", maxHeight: "50px" } },
496
+ onMouseEnter: () => setHoverPost(post.id),
497
+ onMouseLeave: () => setHoverPost(null),
498
+ onClick: () => setPreviewPost(post),
499
+ title: post.content.slice(0, 150)
500
+ },
501
+ // Actions on hover
502
+ isHover && h(
503
+ "div",
504
+ { style: s.cardActions },
505
+ h("button", {
506
+ style: s.cardActionBtn,
507
+ title: "Preview",
508
+ onClick: (e) => {
509
+ e.stopPropagation();
510
+ setPreviewPost(post);
511
+ }
512
+ }, "\u{1F441}"),
513
+ h("button", {
514
+ style: s.cardActionBtn,
515
+ title: "Copy",
516
+ onClick: (e) => {
517
+ e.stopPropagation();
518
+ copyPost(post.content);
519
+ }
520
+ }, "\u{1F4CB}"),
521
+ h("button", {
522
+ style: s.cardActionBtn,
523
+ title: "Delete",
524
+ onClick: (e) => {
525
+ e.stopPropagation();
526
+ deletePost(post.id);
527
+ }
528
+ }, "\u{1F5D1}")
529
+ ),
530
+ // Platform icon + snippet
531
+ driver && h("span", { style: { marginRight: "3px" } }, driver.icon),
532
+ post.content.slice(0, compact ? 25 : 40)
533
+ );
534
+ }
535
+ function WeekView() {
536
+ const weekStart = anchor;
537
+ const days = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i));
538
+ const hours = Array.from({ length: 18 }, (_, i) => i + 6);
539
+ return h(
540
+ "div",
541
+ null,
542
+ // Column headers
543
+ h(
544
+ "div",
545
+ { style: { ...s.weekGrid } },
546
+ h("div", null),
547
+ ...days.map((d, i) => {
548
+ const isToday2 = isSameDay(d, today);
549
+ return h(
550
+ "div",
551
+ { key: i, style: isToday2 ? s.dayColHeaderToday : s.dayColHeader },
552
+ h("div", null, DAY_NAMES[i]),
553
+ h(
554
+ "div",
555
+ { style: isToday2 ? s.dayDateToday : s.dayDate },
556
+ isToday2 && h("span", { style: { marginRight: "4px" } }, "\u25CF"),
557
+ fmt(d)
558
+ )
559
+ );
560
+ })
561
+ ),
562
+ // Time grid
563
+ h(
564
+ "div",
565
+ { style: { ...s.weekGrid, maxHeight: "600px", overflowY: "auto" } },
566
+ ...hours.flatMap((hr) => [
567
+ // Time label
568
+ h(
569
+ "div",
570
+ { key: `t${hr}`, style: s.timeLabel },
571
+ `${hr > 12 ? hr - 12 : hr}:00 ${hr >= 12 ? "PM" : "AM"}`
572
+ ),
573
+ // 7 day cells for this hour
574
+ ...days.map((day, di) => {
575
+ const dayKey = day.toISOString().slice(0, 10);
576
+ const dayPosts = postsForDay(day).filter((p) => {
577
+ const pd = p.scheduledAt ? new Date(p.scheduledAt) : new Date(p.createdAt);
578
+ return pd.getHours() === hr;
579
+ });
580
+ const isFuture = isFutureOrToday(day);
581
+ const hovering = hoverDay === `${dayKey}-${hr}`;
582
+ return h(
583
+ "div",
584
+ {
585
+ key: `${di}-${hr}`,
586
+ style: s.timeSlot,
587
+ onMouseEnter: () => setHoverDay(`${dayKey}-${hr}`),
588
+ onMouseLeave: () => setHoverDay(null)
589
+ },
590
+ ...dayPosts.map((p) => h(PostCard, { key: p.id, post: p, compact: true })),
591
+ // Plus button for future cells
592
+ isFuture && h("button", {
593
+ style: { ...s.plusBtn, ...hovering ? s.plusBtnVisible : {} },
594
+ onClick: () => {
595
+ const d2 = new Date(day);
596
+ d2.setHours(hr, 0, 0, 0);
597
+ openCreateModal(d2.toISOString().slice(0, 16));
598
+ },
599
+ title: "Create post"
600
+ }, "+")
601
+ );
602
+ })
603
+ ])
604
+ )
605
+ );
606
+ }
607
+ function MonthView() {
608
+ const year = anchor.getFullYear();
609
+ const month = anchor.getMonth();
610
+ const firstOfMonth = new Date(year, month, 1);
611
+ let firstDow = firstOfMonth.getDay() - 1;
612
+ if (firstDow < 0) firstDow = 6;
613
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
614
+ const cells = [];
615
+ for (let i = 0; i < firstDow; i++) cells.push(null);
616
+ for (let d = 1; d <= daysInMonth; d++) cells.push(new Date(year, month, d));
617
+ while (cells.length % 7 !== 0) cells.push(null);
618
+ return h(
619
+ "div",
620
+ null,
621
+ // Day headers
622
+ h(
623
+ "div",
624
+ { style: s.monthGrid },
625
+ ...DAY_NAMES_SHORT.map((n) => h("div", { key: n, style: s.monthDayHeader }, n))
626
+ ),
627
+ // Day cells
628
+ h(
629
+ "div",
630
+ { style: s.monthGrid },
631
+ ...cells.map((day, i) => {
632
+ if (!day) return h("div", { key: `e${i}`, style: s.monthCellEmpty });
633
+ const isToday2 = isSameDay(day, today);
634
+ const dayPosts = postsForDay(day);
635
+ const isFuture = isFutureOrToday(day);
636
+ const dayKey = day.toISOString().slice(0, 10);
637
+ const hovering = hoverDay === dayKey;
638
+ return h(
639
+ "div",
640
+ {
641
+ key: i,
642
+ style: s.monthCell,
643
+ onMouseEnter: () => setHoverDay(dayKey),
644
+ onMouseLeave: () => setHoverDay(null)
645
+ },
646
+ h("div", { style: isToday2 ? s.monthDayNumToday : s.monthDayNum }, day.getDate()),
647
+ ...dayPosts.slice(0, 3).map((p) => h(PostCard, { key: p.id, post: p, compact: true })),
648
+ dayPosts.length > 3 && h("div", {
649
+ style: { fontSize: "0.65rem", color: "var(--ck-text-tertiary)", textAlign: "center" }
650
+ }, `+${dayPosts.length - 3} more`),
651
+ // Plus button
652
+ isFuture && h("button", {
653
+ style: { ...s.plusBtn, ...hovering ? s.plusBtnVisible : {} },
654
+ onClick: () => {
655
+ const d2 = new Date(day);
656
+ d2.setHours(17, 0, 0, 0);
657
+ openCreateModal(d2.toISOString().slice(0, 16));
658
+ },
659
+ title: "Create post"
660
+ }, "+")
661
+ );
662
+ })
663
+ )
664
+ );
665
+ }
666
+ function PreviewModal() {
667
+ if (!previewPost) return null;
668
+ const p = previewPost;
669
+ const d = p.scheduledAt ? new Date(p.scheduledAt) : new Date(p.createdAt);
670
+ return h(
671
+ "div",
672
+ { style: s.overlay, onClick: () => setPreviewPost(null) },
673
+ h(
674
+ "div",
675
+ {
676
+ style: { ...s.modal, maxWidth: "550px" },
677
+ onClick: (e) => e.stopPropagation()
678
+ },
679
+ h(
680
+ "div",
681
+ { style: s.modalHeader },
682
+ h("div", { style: { fontWeight: 700, fontSize: "1rem", color: "var(--ck-text-primary)" } }, "Preview Post"),
683
+ h("button", { style: s.closeBtn, onClick: () => setPreviewPost(null) }, "\xD7")
684
+ ),
52
685
  h(
53
686
  "div",
54
- { key: i, style: t.cell },
55
- h("span", { style: isToday ? t.todayBadge : t.dayNum }, day)
687
+ { style: { padding: "1.25rem" } },
688
+ h(
689
+ "div",
690
+ { style: { display: "flex", alignItems: "center", gap: "0.5rem", marginBottom: "0.75rem" } },
691
+ h("div", { style: s.statusDot(p.status) }),
692
+ h("span", { style: { fontSize: "0.8rem", fontWeight: 600, color: "var(--ck-text-secondary)", textTransform: "capitalize" } }, p.status),
693
+ h("span", { style: { fontSize: "0.75rem", color: "var(--ck-text-tertiary)" } }, d.toLocaleString())
694
+ ),
695
+ // Platforms
696
+ p.platforms?.length > 0 && h(
697
+ "div",
698
+ { style: { display: "flex", gap: "0.4rem", marginBottom: "0.75rem", flexWrap: "wrap" } },
699
+ ...p.platforms.map((pl) => {
700
+ const drv = drivers.find((x) => x.platform === pl);
701
+ return h("span", {
702
+ key: pl,
703
+ style: {
704
+ background: "rgba(127,90,240,0.15)",
705
+ border: "1px solid rgba(127,90,240,0.3)",
706
+ borderRadius: "999px",
707
+ padding: "0.15rem 0.5rem",
708
+ fontSize: "0.75rem",
709
+ color: "var(--ck-text-secondary)"
710
+ }
711
+ }, drv ? `${drv.icon} ${drv.label}` : pl);
712
+ })
713
+ ),
714
+ // Content
715
+ h("div", {
716
+ style: {
717
+ ...s.previewPanel,
718
+ whiteSpace: "pre-wrap",
719
+ fontSize: "0.9rem",
720
+ color: "var(--ck-text-primary)",
721
+ lineHeight: "1.5"
722
+ }
723
+ }, p.content),
724
+ // Actions
725
+ h(
726
+ "div",
727
+ { style: { display: "flex", gap: "0.5rem", marginTop: "1rem" } },
728
+ h("button", {
729
+ style: s.btnGhost,
730
+ onClick: () => copyPost(p.content)
731
+ }, "\u{1F4CB} Copy"),
732
+ h("button", {
733
+ style: { ...s.btnGhost, color: "rgba(248,113,113,0.9)" },
734
+ onClick: () => {
735
+ deletePost(p.id);
736
+ setPreviewPost(null);
737
+ }
738
+ }, "\u{1F5D1} Delete")
739
+ )
56
740
  )
57
- );
58
- }
741
+ )
742
+ );
743
+ }
744
+ function CreateModal() {
745
+ if (!modalOpen) return null;
746
+ const charLimit = useMemo(() => {
747
+ if (modalPlatforms.length === 0) return void 0;
748
+ const limits = modalPlatforms.map((p) => drivers.find((d) => d.platform === p)?.capabilities?.maxLength).filter((l) => l !== void 0);
749
+ return limits.length > 0 ? Math.min(...limits) : void 0;
750
+ }, [modalPlatforms, drivers]);
751
+ return h(
752
+ "div",
753
+ { style: s.overlay, onClick: () => setModalOpen(false) },
754
+ h(
755
+ "div",
756
+ {
757
+ style: s.modal,
758
+ onClick: (e) => e.stopPropagation()
759
+ },
760
+ // Header
761
+ h(
762
+ "div",
763
+ { style: s.modalHeader },
764
+ h("div", { style: { fontWeight: 700, fontSize: "1.1rem", color: "var(--ck-text-primary)" } }, "Create Post"),
765
+ h("button", { style: s.closeBtn, onClick: () => setModalOpen(false) }, "\xD7")
766
+ ),
767
+ // Body — two columns
768
+ h(
769
+ "div",
770
+ { style: s.modalBody },
771
+ // Left — compose
772
+ h(
773
+ "div",
774
+ { style: s.modalLeft },
775
+ // Platform circles
776
+ h(
777
+ "div",
778
+ { style: { display: "flex", gap: "0.5rem", flexWrap: "wrap" } },
779
+ ...drivers.map(
780
+ (d) => h("div", {
781
+ key: d.platform,
782
+ style: s.platformCircle(modalPlatforms.includes(d.platform), d.connected),
783
+ onClick: () => {
784
+ if (!d.connected) return;
785
+ setModalPlatforms(
786
+ (prev) => prev.includes(d.platform) ? prev.filter((x) => x !== d.platform) : [...prev, d.platform]
787
+ );
788
+ },
789
+ title: `${d.label}${d.connected ? "" : " (not connected)"}`
790
+ }, d.icon)
791
+ )
792
+ ),
793
+ // Textarea
794
+ h("textarea", {
795
+ style: s.textarea,
796
+ value: modalContent,
797
+ onChange: (e) => setModalContent(e.target.value),
798
+ placeholder: "Write something \u2026",
799
+ autoFocus: true
800
+ }),
801
+ // Toolbar
802
+ h(
803
+ "div",
804
+ { style: { display: "flex", gap: "0.5rem", alignItems: "center", flexWrap: "wrap" } },
805
+ h("button", {
806
+ style: { ...s.btnGhost, padding: "0.3rem 0.6rem", fontSize: "0.75rem" },
807
+ onClick: () => setModalMediaUrl(modalMediaUrl ? "" : " ")
808
+ }, "\u{1F5BC} Insert Media"),
809
+ charLimit && h("span", {
810
+ style: {
811
+ fontSize: "0.75rem",
812
+ marginLeft: "auto",
813
+ color: modalContent.length > charLimit ? "rgba(248,113,113,0.95)" : "var(--ck-text-tertiary)"
814
+ }
815
+ }, `${modalContent.length}/${charLimit}`)
816
+ ),
817
+ // Media URL input
818
+ modalMediaUrl !== "" && h("input", {
819
+ style: s.input,
820
+ type: "url",
821
+ value: modalMediaUrl.trim(),
822
+ onChange: (e) => setModalMediaUrl(e.target.value),
823
+ placeholder: "Paste image or video URL\u2026"
824
+ })
825
+ ),
826
+ // Right — preview
827
+ h(
828
+ "div",
829
+ { style: s.modalRight },
830
+ h("div", { style: { fontWeight: 600, fontSize: "0.9rem", color: "var(--ck-text-primary)", marginBottom: "0.5rem" } }, "Post Preview"),
831
+ modalContent.trim() ? h("div", { style: { ...s.previewPanel, whiteSpace: "pre-wrap", fontSize: "0.85rem", color: "var(--ck-text-secondary)" } }, modalContent) : h("div", { style: { color: "var(--ck-text-tertiary)", fontSize: "0.85rem" } }, "Start writing your post for a preview")
832
+ )
833
+ ),
834
+ // Footer
835
+ h(
836
+ "div",
837
+ { style: s.modalFooter },
838
+ // Left — date
839
+ h(
840
+ "div",
841
+ { style: { display: "flex", alignItems: "center", gap: "0.5rem" } },
842
+ h("span", { style: { fontSize: "0.8rem", color: "var(--ck-text-tertiary)" } }, "\u{1F4C5}"),
843
+ h("input", {
844
+ style: { ...s.input, width: "220px" },
845
+ type: "datetime-local",
846
+ value: modalDate,
847
+ onChange: (e) => setModalDate(e.target.value)
848
+ })
849
+ ),
850
+ // Right — buttons
851
+ h(
852
+ "div",
853
+ { style: { display: "flex", gap: "0.5rem" } },
854
+ h("button", {
855
+ style: { ...s.btnGhost, opacity: modalSaving ? 0.6 : 1 },
856
+ onClick: () => void modalSaveDraft(),
857
+ disabled: modalSaving || !modalContent.trim()
858
+ }, modalSaving ? "Saving\u2026" : "Save as draft"),
859
+ connectedDrivers.length > 0 && modalPlatforms.length > 0 && h("button", {
860
+ style: { ...s.btnPrimary, opacity: modalPublishing ? 0.6 : 1 },
861
+ onClick: () => void modalPublish(),
862
+ disabled: modalPublishing || !modalContent.trim()
863
+ }, modalPublishing ? "Publishing\u2026" : modalDate ? "\u23F1 Schedule" : "\u{1F4E4} Publish")
864
+ )
865
+ ),
866
+ // Error / success
867
+ (modalError || modalSuccess) && h("div", {
868
+ style: {
869
+ padding: "0.5rem 1.25rem",
870
+ fontSize: "0.8rem",
871
+ color: modalError ? "rgba(248,113,113,0.95)" : "rgba(74,222,128,0.9)"
872
+ }
873
+ }, modalError || modalSuccess)
874
+ )
875
+ );
59
876
  }
877
+ const dateRangeLabel = useMemo(() => {
878
+ if (view === "week") {
879
+ const end = addDays(anchor, 6);
880
+ return `${fmt(anchor)} \u2013 ${fmt(end)}`;
881
+ }
882
+ return `${MONTH_NAMES[anchor.getMonth()]} ${anchor.getFullYear()}`;
883
+ }, [view, anchor]);
60
884
  return h(
61
885
  "div",
62
- { style: t.card },
63
- h("div", { className: "text-sm font-medium mb-3", style: t.text }, monthName),
886
+ { style: s.container },
887
+ // Top bar
64
888
  h(
65
889
  "div",
66
- { style: { ...grid7, marginBottom: "3px" } },
67
- ...dayNames.map((d) => h("div", { key: d, className: "text-center py-1", style: t.dayHeader }, d))
890
+ { style: s.topBar },
891
+ h(
892
+ "div",
893
+ { style: s.navGroup },
894
+ h("button", { style: s.navBtn, onClick: goPrev }, "\u2039"),
895
+ h("span", { style: s.dateLabel }, dateRangeLabel),
896
+ h("button", { style: s.navBtn, onClick: goNext }, "\u203A"),
897
+ h("button", { style: s.navBtn, onClick: goToday }, "Today")
898
+ ),
899
+ h(
900
+ "div",
901
+ { style: s.navGroup },
902
+ h("button", { style: view === "week" ? s.navBtnActive : s.navBtn, onClick: () => {
903
+ setView("week");
904
+ setAnchor(startOfWeek(today));
905
+ } }, "Week"),
906
+ h("button", { style: view === "month" ? s.navBtnActive : s.navBtn, onClick: () => {
907
+ setView("month");
908
+ setAnchor(new Date(today.getFullYear(), today.getMonth(), 1));
909
+ } }, "Month"),
910
+ h("button", { style: s.btnPrimary, onClick: () => openCreateModal() }, "+ New Post")
911
+ )
68
912
  ),
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.")
913
+ loading ? h("div", { style: { textAlign: "center", padding: "3rem", color: "var(--ck-text-tertiary)" } }, "Loading calendar\u2026") : view === "week" ? h(WeekView, null) : h(MonthView, null),
914
+ h(PreviewModal, null),
915
+ h(CreateModal, null)
71
916
  );
72
917
  }
73
918
  window.KitchenPlugin.registerTab("marketing", "content-calendar", ContentCalendar);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiggai/kitchen-plugin-marketing",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "Marketing Suite plugin for ClawKitchen",
5
5
  "main": "dist/index.js",
6
6
  "files": [