social-light 0.0.1
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/.env.example +7 -0
- package/.instructions/checklist.md +45 -0
- package/.instructions/prd.md +182 -0
- package/.instructions/summary.md +122 -0
- package/README.md +196 -0
- package/bin/socialite +7 -0
- package/delete/tiktok.mjs +315 -0
- package/delete/twitter.mjs +258 -0
- package/package.json +51 -0
- package/server.png +0 -0
- package/src/commands/create.mjs +274 -0
- package/src/commands/edit.mjs +198 -0
- package/src/commands/init.mjs +256 -0
- package/src/commands/publish.mjs +192 -0
- package/src/commands/published.mjs +90 -0
- package/src/commands/unpublished.mjs +102 -0
- package/src/index.mjs +107 -0
- package/src/server/client/index.html +41 -0
- package/src/server/client/logo.jpg +0 -0
- package/src/server/client/main.mjs +953 -0
- package/src/server/client/styles.css +535 -0
- package/src/server/index.mjs +315 -0
- package/src/utils/ai.mjs +201 -0
- package/src/utils/config.mjs +127 -0
- package/src/utils/db.mjs +260 -0
- package/src/utils/fix-config.mjs +60 -0
- package/src/utils/social/base.mjs +142 -0
- package/src/utils/social/bluesky.mjs +302 -0
- package/src/utils/social/index.mjs +300 -0
@@ -0,0 +1,953 @@
|
|
1
|
+
// Main entry point for the Social Light web client
|
2
|
+
|
3
|
+
// State management
|
4
|
+
const state = {
|
5
|
+
posts: [],
|
6
|
+
unpublishedPosts: [],
|
7
|
+
publishedPosts: [],
|
8
|
+
currentView: "unpublished", // 'unpublished', 'published', 'editor', 'calendar'
|
9
|
+
currentPost: null,
|
10
|
+
config: null,
|
11
|
+
loading: true,
|
12
|
+
error: null,
|
13
|
+
};
|
14
|
+
|
15
|
+
// DOM elements
|
16
|
+
let app;
|
17
|
+
let mainContent;
|
18
|
+
|
19
|
+
// Initialize the application
|
20
|
+
const init = async () => {
|
21
|
+
app = document.getElementById("app");
|
22
|
+
|
23
|
+
// Fetch configuration
|
24
|
+
try {
|
25
|
+
state.loading = true;
|
26
|
+
renderApp();
|
27
|
+
|
28
|
+
// Fetch configuration
|
29
|
+
const configResponse = await fetch("/api/config");
|
30
|
+
if (!configResponse.ok) throw new Error("Failed to load configuration");
|
31
|
+
state.config = await configResponse.json();
|
32
|
+
|
33
|
+
// Fetch posts
|
34
|
+
await fetchPosts();
|
35
|
+
|
36
|
+
state.loading = false;
|
37
|
+
renderApp();
|
38
|
+
} catch (error) {
|
39
|
+
console.error("Initialization error:", error);
|
40
|
+
state.error = error.message;
|
41
|
+
state.loading = false;
|
42
|
+
renderApp();
|
43
|
+
}
|
44
|
+
};
|
45
|
+
|
46
|
+
// Fetch posts from API
|
47
|
+
const fetchPosts = async () => {
|
48
|
+
try {
|
49
|
+
// Fetch unpublished posts
|
50
|
+
const unpublishedResponse = await fetch("/api/posts?published=false");
|
51
|
+
if (!unpublishedResponse.ok)
|
52
|
+
throw new Error("Failed to fetch unpublished posts");
|
53
|
+
state.unpublishedPosts = await unpublishedResponse.json();
|
54
|
+
|
55
|
+
// Fetch published posts
|
56
|
+
const publishedResponse = await fetch("/api/posts?published=true");
|
57
|
+
if (!publishedResponse.ok)
|
58
|
+
throw new Error("Failed to fetch published posts");
|
59
|
+
state.publishedPosts = await publishedResponse.json();
|
60
|
+
|
61
|
+
// Set combined posts
|
62
|
+
state.posts = [...state.unpublishedPosts, ...state.publishedPosts];
|
63
|
+
} catch (error) {
|
64
|
+
console.error("Error fetching posts:", error);
|
65
|
+
state.error = error.message;
|
66
|
+
}
|
67
|
+
};
|
68
|
+
|
69
|
+
// Render the application
|
70
|
+
const renderApp = () => {
|
71
|
+
if (state.loading) {
|
72
|
+
app.innerHTML = `
|
73
|
+
<div class="loading">
|
74
|
+
<div class="spinner"></div>
|
75
|
+
<p>Loading Social Light...</p>
|
76
|
+
</div>
|
77
|
+
`;
|
78
|
+
return;
|
79
|
+
}
|
80
|
+
|
81
|
+
if (state.error) {
|
82
|
+
app.innerHTML = `
|
83
|
+
<div class="card">
|
84
|
+
<h2 class="text-center" style="color: var(--color-accent-danger)">Error</h2>
|
85
|
+
<p class="text-center">${state.error}</p>
|
86
|
+
<div class="text-center mt-md">
|
87
|
+
<button class="btn btn-primary" onclick="location.reload()">Retry</button>
|
88
|
+
</div>
|
89
|
+
</div>
|
90
|
+
`;
|
91
|
+
return;
|
92
|
+
}
|
93
|
+
|
94
|
+
// Render application layout
|
95
|
+
app.innerHTML = `
|
96
|
+
<header class="header">
|
97
|
+
|
98
|
+
<div class="header-logo">Social Light</div>
|
99
|
+
<nav class="header-nav">
|
100
|
+
<button class="btn ${
|
101
|
+
state.currentView === "unpublished" ? "btn-primary" : ""
|
102
|
+
}"
|
103
|
+
data-view="unpublished">Unpublished</button>
|
104
|
+
<button class="btn ${
|
105
|
+
state.currentView === "published" ? "btn-primary" : ""
|
106
|
+
}"
|
107
|
+
data-view="published">Published</button>
|
108
|
+
<button class="btn ${
|
109
|
+
state.currentView === "calendar" ? "btn-primary" : ""
|
110
|
+
}"
|
111
|
+
data-view="calendar">Calendar</button>
|
112
|
+
<button class="btn btn-action" data-action="create-post">Create Post</button>
|
113
|
+
</nav>
|
114
|
+
</header>
|
115
|
+
|
116
|
+
<main id="main-content"></main>
|
117
|
+
|
118
|
+
<footer class="footer">
|
119
|
+
<p>Social Light - AI-powered social media scheduler</p>
|
120
|
+
</footer>
|
121
|
+
`;
|
122
|
+
|
123
|
+
// Set up event listeners for navigation
|
124
|
+
document.querySelectorAll("[data-view]").forEach((button) => {
|
125
|
+
button.addEventListener("click", () => {
|
126
|
+
state.currentView = button.dataset.view;
|
127
|
+
renderApp();
|
128
|
+
});
|
129
|
+
});
|
130
|
+
|
131
|
+
// Set up event listener for create post button
|
132
|
+
document
|
133
|
+
.querySelector('[data-action="create-post"]')
|
134
|
+
.addEventListener("click", () => {
|
135
|
+
state.currentView = "editor";
|
136
|
+
state.currentPost = null;
|
137
|
+
renderApp();
|
138
|
+
});
|
139
|
+
|
140
|
+
// Render main content based on current view
|
141
|
+
mainContent = document.getElementById("main-content");
|
142
|
+
renderMainContent();
|
143
|
+
};
|
144
|
+
|
145
|
+
// Render main content based on current view
|
146
|
+
const renderMainContent = () => {
|
147
|
+
switch (state.currentView) {
|
148
|
+
case "unpublished":
|
149
|
+
renderUnpublishedPosts();
|
150
|
+
break;
|
151
|
+
case "published":
|
152
|
+
renderPublishedPosts();
|
153
|
+
break;
|
154
|
+
case "editor":
|
155
|
+
renderPostEditor();
|
156
|
+
break;
|
157
|
+
case "calendar":
|
158
|
+
renderCalendar();
|
159
|
+
break;
|
160
|
+
default:
|
161
|
+
renderUnpublishedPosts();
|
162
|
+
}
|
163
|
+
};
|
164
|
+
|
165
|
+
// Render list of unpublished posts
|
166
|
+
const renderUnpublishedPosts = () => {
|
167
|
+
if (state.unpublishedPosts.length === 0) {
|
168
|
+
mainContent.innerHTML = `
|
169
|
+
<div class="card text-center">
|
170
|
+
<h2>No Unpublished Posts</h2>
|
171
|
+
<p>You don't have any unpublished posts yet.</p>
|
172
|
+
<button class="btn btn-action mt-md" data-action="create-post">Create New Post</button>
|
173
|
+
</div>
|
174
|
+
`;
|
175
|
+
|
176
|
+
// Set up event listener for create post button
|
177
|
+
mainContent
|
178
|
+
.querySelector('[data-action="create-post"]')
|
179
|
+
.addEventListener("click", () => {
|
180
|
+
state.currentView = "editor";
|
181
|
+
state.currentPost = null;
|
182
|
+
renderApp();
|
183
|
+
});
|
184
|
+
|
185
|
+
return;
|
186
|
+
}
|
187
|
+
|
188
|
+
mainContent.innerHTML = `
|
189
|
+
<div class="section">
|
190
|
+
<div class="d-flex justify-between align-center mb-md">
|
191
|
+
<h2>Unpublished Posts</h2>
|
192
|
+
<button class="btn btn-action" data-action="create-post">Create New Post</button>
|
193
|
+
</div>
|
194
|
+
|
195
|
+
<div class="post-list">
|
196
|
+
${state.unpublishedPosts
|
197
|
+
.map(
|
198
|
+
(post) => `
|
199
|
+
<div class="card post-card" data-post-id="${post.id}">
|
200
|
+
<div class="post-card-header">
|
201
|
+
<h3 class="post-card-title">${post.title || "Untitled"}</h3>
|
202
|
+
<div class="post-card-date">${formatDate(post.publish_date)}</div>
|
203
|
+
</div>
|
204
|
+
<div class="post-card-content">
|
205
|
+
${post.content}
|
206
|
+
</div>
|
207
|
+
<div class="post-card-footer">
|
208
|
+
<div class="post-card-platforms">
|
209
|
+
${formatPlatforms(post.platforms)}
|
210
|
+
</div>
|
211
|
+
<div class="d-flex gap-sm">
|
212
|
+
<button class="btn btn-sm" data-action="edit-post" data-post-id="${
|
213
|
+
post.id
|
214
|
+
}">Edit</button>
|
215
|
+
<button class="btn btn-sm btn-action" data-action="publish-post" data-post-id="${
|
216
|
+
post.id
|
217
|
+
}">Publish</button>
|
218
|
+
</div>
|
219
|
+
</div>
|
220
|
+
</div>
|
221
|
+
`
|
222
|
+
)
|
223
|
+
.join("")}
|
224
|
+
</div>
|
225
|
+
</div>
|
226
|
+
`;
|
227
|
+
|
228
|
+
// Set up event listeners
|
229
|
+
// Edit post buttons
|
230
|
+
mainContent
|
231
|
+
.querySelectorAll('[data-action="edit-post"]')
|
232
|
+
.forEach((button) => {
|
233
|
+
button.addEventListener("click", () => {
|
234
|
+
const postId = parseInt(button.dataset.postId, 10);
|
235
|
+
const post = state.posts.find((p) => p.id === postId);
|
236
|
+
if (post) {
|
237
|
+
state.currentView = "editor";
|
238
|
+
state.currentPost = post;
|
239
|
+
renderApp();
|
240
|
+
}
|
241
|
+
});
|
242
|
+
});
|
243
|
+
|
244
|
+
// Publish post buttons
|
245
|
+
mainContent
|
246
|
+
.querySelectorAll('[data-action="publish-post"]')
|
247
|
+
.forEach((button) => {
|
248
|
+
button.addEventListener("click", async () => {
|
249
|
+
const postId = parseInt(button.dataset.postId, 10);
|
250
|
+
await publishPost(postId);
|
251
|
+
});
|
252
|
+
});
|
253
|
+
|
254
|
+
// Create post button
|
255
|
+
mainContent
|
256
|
+
.querySelector('[data-action="create-post"]')
|
257
|
+
.addEventListener("click", () => {
|
258
|
+
state.currentView = "editor";
|
259
|
+
state.currentPost = null;
|
260
|
+
renderApp();
|
261
|
+
});
|
262
|
+
};
|
263
|
+
|
264
|
+
// Render list of published posts
|
265
|
+
const renderPublishedPosts = () => {
|
266
|
+
if (state.publishedPosts.length === 0) {
|
267
|
+
mainContent.innerHTML = `
|
268
|
+
<div class="card text-center">
|
269
|
+
<h2>No Published Posts</h2>
|
270
|
+
<p>You haven't published any posts yet.</p>
|
271
|
+
<div class="mt-md">
|
272
|
+
<button class="btn btn-primary" data-view="unpublished">View Unpublished Posts</button>
|
273
|
+
</div>
|
274
|
+
</div>
|
275
|
+
`;
|
276
|
+
|
277
|
+
// Set up event listener for view unpublished button
|
278
|
+
mainContent
|
279
|
+
.querySelector('[data-view="unpublished"]')
|
280
|
+
.addEventListener("click", () => {
|
281
|
+
state.currentView = "unpublished";
|
282
|
+
renderApp();
|
283
|
+
});
|
284
|
+
|
285
|
+
return;
|
286
|
+
}
|
287
|
+
|
288
|
+
mainContent.innerHTML = `
|
289
|
+
<div class="section">
|
290
|
+
<h2 class="mb-md">Published Posts</h2>
|
291
|
+
|
292
|
+
<div class="post-list">
|
293
|
+
${state.publishedPosts
|
294
|
+
.map(
|
295
|
+
(post) => `
|
296
|
+
<div class="card post-card">
|
297
|
+
<div class="post-card-header">
|
298
|
+
<h3 class="post-card-title">${post.title || "Untitled"}</h3>
|
299
|
+
<div class="post-card-date">${formatDate(post.publish_date)}</div>
|
300
|
+
</div>
|
301
|
+
<div class="post-card-content">
|
302
|
+
${post.content}
|
303
|
+
</div>
|
304
|
+
<div class="post-card-footer">
|
305
|
+
<div class="post-card-platforms">
|
306
|
+
${formatPlatforms(post.platforms)}
|
307
|
+
</div>
|
308
|
+
<div>
|
309
|
+
<span class="text-secondary">Published</span>
|
310
|
+
</div>
|
311
|
+
</div>
|
312
|
+
</div>
|
313
|
+
`
|
314
|
+
)
|
315
|
+
.join("")}
|
316
|
+
</div>
|
317
|
+
</div>
|
318
|
+
`;
|
319
|
+
};
|
320
|
+
|
321
|
+
// Render post editor
|
322
|
+
const renderPostEditor = () => {
|
323
|
+
const isEditing = Boolean(state.currentPost);
|
324
|
+
const post = state.currentPost || {
|
325
|
+
title: "",
|
326
|
+
content: "",
|
327
|
+
platforms: "",
|
328
|
+
publish_date: "",
|
329
|
+
};
|
330
|
+
|
331
|
+
// Get available platforms from config
|
332
|
+
const platformOptions = state.config.platforms || [
|
333
|
+
{ id: "twitter", name: "Twitter" },
|
334
|
+
{ id: "bluesky", name: "Bluesky" },
|
335
|
+
{ id: "tiktok", name: "TikTok" },
|
336
|
+
];
|
337
|
+
|
338
|
+
// Get selected platforms
|
339
|
+
const selectedPlatforms = post.platforms
|
340
|
+
? post.platforms.split(",").map((p) => p.trim())
|
341
|
+
: [];
|
342
|
+
|
343
|
+
mainContent.innerHTML = `
|
344
|
+
<div class="card post-editor">
|
345
|
+
<div class="post-editor-header">
|
346
|
+
<h2 class="post-editor-title">${
|
347
|
+
isEditing ? "Edit Post" : "Create New Post"
|
348
|
+
}</h2>
|
349
|
+
</div>
|
350
|
+
|
351
|
+
<form id="post-form">
|
352
|
+
<div class="form-group">
|
353
|
+
<label class="form-label" for="post-title">Title</label>
|
354
|
+
<div class="d-flex gap-sm">
|
355
|
+
<input
|
356
|
+
type="text"
|
357
|
+
id="post-title"
|
358
|
+
class="form-control"
|
359
|
+
value="${post.title || ""}"
|
360
|
+
placeholder="Enter title (or leave blank for AI to generate)"
|
361
|
+
>
|
362
|
+
${
|
363
|
+
state.config.aiEnabled
|
364
|
+
? `
|
365
|
+
<button type="button" class="btn" id="generate-title-btn">Generate with AI</button>
|
366
|
+
`
|
367
|
+
: ""
|
368
|
+
}
|
369
|
+
</div>
|
370
|
+
</div>
|
371
|
+
|
372
|
+
<div class="form-group">
|
373
|
+
<label class="form-label" for="post-content">Content</label>
|
374
|
+
<textarea
|
375
|
+
id="post-content"
|
376
|
+
class="form-control"
|
377
|
+
placeholder="What's on your mind?"
|
378
|
+
rows="6"
|
379
|
+
>${post.content || ""}</textarea>
|
380
|
+
${
|
381
|
+
state.config.aiEnabled && platformOptions.length > 0
|
382
|
+
? `
|
383
|
+
<div class="d-flex justify-end mt-sm">
|
384
|
+
<button type="button" class="btn" id="enhance-content-btn">Enhance with AI</button>
|
385
|
+
</div>
|
386
|
+
`
|
387
|
+
: ""
|
388
|
+
}
|
389
|
+
</div>
|
390
|
+
|
391
|
+
<div class="form-group">
|
392
|
+
<label class="form-label" for="post-date">Publish Date</label>
|
393
|
+
<div class="d-flex gap-sm">
|
394
|
+
<input
|
395
|
+
type="date"
|
396
|
+
id="post-date"
|
397
|
+
class="form-control"
|
398
|
+
value="${post.publish_date || ""}"
|
399
|
+
placeholder="YYYY-MM-DD"
|
400
|
+
>
|
401
|
+
${
|
402
|
+
state.config.aiEnabled
|
403
|
+
? `
|
404
|
+
<button type="button" class="btn" id="suggest-date-btn">Suggest with AI</button>
|
405
|
+
`
|
406
|
+
: ""
|
407
|
+
}
|
408
|
+
</div>
|
409
|
+
</div>
|
410
|
+
|
411
|
+
<div class="form-group">
|
412
|
+
<label class="form-label">Platforms</label>
|
413
|
+
<div class="d-flex gap-md flex-wrap">
|
414
|
+
${platformOptions
|
415
|
+
.map(
|
416
|
+
(platform) => `
|
417
|
+
<label class="d-flex align-center gap-sm">
|
418
|
+
<input
|
419
|
+
type="checkbox"
|
420
|
+
name="platforms"
|
421
|
+
value="${platform.id}"
|
422
|
+
${selectedPlatforms.includes(platform.id) ? "checked" : ""}
|
423
|
+
>
|
424
|
+
${platform.name}
|
425
|
+
</label>
|
426
|
+
`
|
427
|
+
)
|
428
|
+
.join("")}
|
429
|
+
</div>
|
430
|
+
</div>
|
431
|
+
|
432
|
+
<div class="post-editor-actions">
|
433
|
+
<button type="button" class="btn" id="cancel-btn">Cancel</button>
|
434
|
+
<button type="submit" class="btn btn-primary">${
|
435
|
+
isEditing ? "Update" : "Create"
|
436
|
+
}</button>
|
437
|
+
</div>
|
438
|
+
</form>
|
439
|
+
</div>
|
440
|
+
`;
|
441
|
+
|
442
|
+
// Set up event listeners
|
443
|
+
|
444
|
+
// Cancel button
|
445
|
+
document.getElementById("cancel-btn").addEventListener("click", () => {
|
446
|
+
state.currentView = "unpublished";
|
447
|
+
state.currentPost = null;
|
448
|
+
renderApp();
|
449
|
+
});
|
450
|
+
|
451
|
+
// Form submission
|
452
|
+
document
|
453
|
+
.getElementById("post-form")
|
454
|
+
.addEventListener("submit", async (event) => {
|
455
|
+
event.preventDefault();
|
456
|
+
|
457
|
+
// Get form values
|
458
|
+
const title = document.getElementById("post-title").value;
|
459
|
+
const content = document.getElementById("post-content").value;
|
460
|
+
const publishDate = document.getElementById("post-date").value;
|
461
|
+
const platformElements = document.querySelectorAll(
|
462
|
+
'input[name="platforms"]:checked'
|
463
|
+
);
|
464
|
+
const platforms = Array.from(platformElements)
|
465
|
+
.map((el) => el.value)
|
466
|
+
.join(",");
|
467
|
+
|
468
|
+
// Validate form
|
469
|
+
if (!content) {
|
470
|
+
alert("Please enter post content");
|
471
|
+
return;
|
472
|
+
}
|
473
|
+
|
474
|
+
try {
|
475
|
+
if (isEditing) {
|
476
|
+
// Update existing post
|
477
|
+
const response = await fetch(`/api/posts/${post.id}`, {
|
478
|
+
method: "PUT",
|
479
|
+
headers: {
|
480
|
+
"Content-Type": "application/json",
|
481
|
+
},
|
482
|
+
body: JSON.stringify({
|
483
|
+
title,
|
484
|
+
content,
|
485
|
+
platforms,
|
486
|
+
publish_date: publishDate,
|
487
|
+
}),
|
488
|
+
});
|
489
|
+
|
490
|
+
if (!response.ok) {
|
491
|
+
const error = await response.json();
|
492
|
+
throw new Error(error.error || "Failed to update post");
|
493
|
+
}
|
494
|
+
|
495
|
+
// Update local state
|
496
|
+
const updatedPost = {
|
497
|
+
...post,
|
498
|
+
title,
|
499
|
+
content,
|
500
|
+
platforms,
|
501
|
+
publish_date: publishDate,
|
502
|
+
};
|
503
|
+
|
504
|
+
// Update in posts arrays
|
505
|
+
const postIndex = state.posts.findIndex((p) => p.id === post.id);
|
506
|
+
if (postIndex !== -1) state.posts[postIndex] = updatedPost;
|
507
|
+
|
508
|
+
const unpubIndex = state.unpublishedPosts.findIndex(
|
509
|
+
(p) => p.id === post.id
|
510
|
+
);
|
511
|
+
if (unpubIndex !== -1)
|
512
|
+
state.unpublishedPosts[unpubIndex] = updatedPost;
|
513
|
+
} else {
|
514
|
+
// Create new post
|
515
|
+
const response = await fetch("/api/posts", {
|
516
|
+
method: "POST",
|
517
|
+
headers: {
|
518
|
+
"Content-Type": "application/json",
|
519
|
+
},
|
520
|
+
body: JSON.stringify({
|
521
|
+
title,
|
522
|
+
content,
|
523
|
+
platforms,
|
524
|
+
publish_date: publishDate,
|
525
|
+
}),
|
526
|
+
});
|
527
|
+
|
528
|
+
if (!response.ok) {
|
529
|
+
const error = await response.json();
|
530
|
+
throw new Error(error.error || "Failed to create post");
|
531
|
+
}
|
532
|
+
|
533
|
+
// Refresh posts
|
534
|
+
await fetchPosts();
|
535
|
+
}
|
536
|
+
|
537
|
+
// Return to posts view
|
538
|
+
state.currentView = "unpublished";
|
539
|
+
state.currentPost = null;
|
540
|
+
renderApp();
|
541
|
+
} catch (error) {
|
542
|
+
console.error("Error saving post:", error);
|
543
|
+
alert(`Error: ${error.message}`);
|
544
|
+
}
|
545
|
+
});
|
546
|
+
|
547
|
+
// Enhance content with AI
|
548
|
+
const enhanceContentBtn = document.getElementById("enhance-content-btn");
|
549
|
+
if (enhanceContentBtn) {
|
550
|
+
enhanceContentBtn.addEventListener("click", async () => {
|
551
|
+
const content = document.getElementById("post-content").value;
|
552
|
+
const platformCheckboxes = document.querySelectorAll(
|
553
|
+
'input[name="platforms"]:checked'
|
554
|
+
);
|
555
|
+
|
556
|
+
if (!content) {
|
557
|
+
alert("Please enter post content first");
|
558
|
+
return;
|
559
|
+
}
|
560
|
+
|
561
|
+
if (platformCheckboxes.length === 0) {
|
562
|
+
alert("Please select at least one platform");
|
563
|
+
return;
|
564
|
+
}
|
565
|
+
|
566
|
+
// Get the first selected platform
|
567
|
+
const platform = platformCheckboxes[0].value;
|
568
|
+
|
569
|
+
try {
|
570
|
+
enhanceContentBtn.disabled = true;
|
571
|
+
enhanceContentBtn.textContent = "Enhancing...";
|
572
|
+
|
573
|
+
const response = await fetch("/api/ai/enhance", {
|
574
|
+
method: "POST",
|
575
|
+
headers: {
|
576
|
+
"Content-Type": "application/json",
|
577
|
+
},
|
578
|
+
body: JSON.stringify({ content, platform }),
|
579
|
+
});
|
580
|
+
|
581
|
+
if (!response.ok) {
|
582
|
+
const error = await response.json();
|
583
|
+
throw new Error(error.error || "Failed to enhance content");
|
584
|
+
}
|
585
|
+
|
586
|
+
const result = await response.json();
|
587
|
+
const enhancedContent = result.enhanced;
|
588
|
+
|
589
|
+
if (enhancedContent === content) {
|
590
|
+
alert("No significant enhancements suggested");
|
591
|
+
} else {
|
592
|
+
// Create a modal to show both versions for comparison
|
593
|
+
const modalHtml = `
|
594
|
+
<div class="modal-overlay" id="content-comparison-modal">
|
595
|
+
<div class="modal-content">
|
596
|
+
<div class="modal-header">
|
597
|
+
<h3>Content Comparison</h3>
|
598
|
+
<button type="button" class="btn-close" id="close-modal">×</button>
|
599
|
+
</div>
|
600
|
+
<div class="modal-body">
|
601
|
+
<div class="comparison-container">
|
602
|
+
<div class="comparison-column">
|
603
|
+
<h4>Original</h4>
|
604
|
+
<div class="comparison-content original-content">${content}</div>
|
605
|
+
</div>
|
606
|
+
<div class="comparison-column">
|
607
|
+
<h4>Enhanced for ${platform}</h4>
|
608
|
+
<div class="comparison-content enhanced-content">${enhancedContent}</div>
|
609
|
+
</div>
|
610
|
+
</div>
|
611
|
+
</div>
|
612
|
+
<div class="modal-footer">
|
613
|
+
<button type="button" class="btn" id="keep-original-btn">Keep Original</button>
|
614
|
+
<button type="button" class="btn btn-primary" id="use-enhanced-btn">Use Enhanced</button>
|
615
|
+
</div>
|
616
|
+
</div>
|
617
|
+
</div>
|
618
|
+
`;
|
619
|
+
|
620
|
+
// Add modal to the DOM
|
621
|
+
const modalContainer = document.createElement("div");
|
622
|
+
modalContainer.innerHTML = modalHtml;
|
623
|
+
document.body.appendChild(modalContainer.firstElementChild);
|
624
|
+
|
625
|
+
// Add event listeners for the modal
|
626
|
+
document
|
627
|
+
.getElementById("close-modal")
|
628
|
+
.addEventListener("click", () => {
|
629
|
+
document.getElementById("content-comparison-modal").remove();
|
630
|
+
});
|
631
|
+
|
632
|
+
document
|
633
|
+
.getElementById("keep-original-btn")
|
634
|
+
.addEventListener("click", () => {
|
635
|
+
document.getElementById("content-comparison-modal").remove();
|
636
|
+
});
|
637
|
+
|
638
|
+
document
|
639
|
+
.getElementById("use-enhanced-btn")
|
640
|
+
.addEventListener("click", () => {
|
641
|
+
document.getElementById("post-content").value = enhancedContent;
|
642
|
+
document.getElementById("content-comparison-modal").remove();
|
643
|
+
});
|
644
|
+
}
|
645
|
+
} catch (error) {
|
646
|
+
console.error("Error enhancing content:", error);
|
647
|
+
alert(`Error: ${error.message}`);
|
648
|
+
} finally {
|
649
|
+
enhanceContentBtn.disabled = false;
|
650
|
+
enhanceContentBtn.textContent = "Enhance with AI";
|
651
|
+
}
|
652
|
+
});
|
653
|
+
}
|
654
|
+
|
655
|
+
// Generate title with AI
|
656
|
+
const generateTitleBtn = document.getElementById("generate-title-btn");
|
657
|
+
if (generateTitleBtn) {
|
658
|
+
generateTitleBtn.addEventListener("click", async () => {
|
659
|
+
const content = document.getElementById("post-content").value;
|
660
|
+
|
661
|
+
if (!content) {
|
662
|
+
alert("Please enter post content first");
|
663
|
+
return;
|
664
|
+
}
|
665
|
+
|
666
|
+
try {
|
667
|
+
generateTitleBtn.disabled = true;
|
668
|
+
generateTitleBtn.textContent = "Generating...";
|
669
|
+
|
670
|
+
const response = await fetch("/api/ai/title", {
|
671
|
+
method: "POST",
|
672
|
+
headers: {
|
673
|
+
"Content-Type": "application/json",
|
674
|
+
},
|
675
|
+
body: JSON.stringify({ content }),
|
676
|
+
});
|
677
|
+
|
678
|
+
if (!response.ok) {
|
679
|
+
const error = await response.json();
|
680
|
+
throw new Error(error.error || "Failed to generate title");
|
681
|
+
}
|
682
|
+
|
683
|
+
const result = await response.json();
|
684
|
+
document.getElementById("post-title").value = result.title;
|
685
|
+
} catch (error) {
|
686
|
+
console.error("Error generating title:", error);
|
687
|
+
alert(`Error: ${error.message}`);
|
688
|
+
} finally {
|
689
|
+
generateTitleBtn.disabled = false;
|
690
|
+
generateTitleBtn.textContent = "Generate with AI";
|
691
|
+
}
|
692
|
+
});
|
693
|
+
}
|
694
|
+
|
695
|
+
// Suggest date with AI
|
696
|
+
const suggestDateBtn = document.getElementById("suggest-date-btn");
|
697
|
+
if (suggestDateBtn) {
|
698
|
+
suggestDateBtn.addEventListener("click", async () => {
|
699
|
+
try {
|
700
|
+
suggestDateBtn.disabled = true;
|
701
|
+
suggestDateBtn.textContent = "Generating...";
|
702
|
+
|
703
|
+
const response = await fetch("/api/ai/date");
|
704
|
+
|
705
|
+
if (!response.ok) {
|
706
|
+
const error = await response.json();
|
707
|
+
throw new Error(error.error || "Failed to suggest date");
|
708
|
+
}
|
709
|
+
|
710
|
+
const result = await response.json();
|
711
|
+
document.getElementById("post-date").value = result.date;
|
712
|
+
} catch (error) {
|
713
|
+
console.error("Error suggesting date:", error);
|
714
|
+
alert(`Error: ${error.message}`);
|
715
|
+
} finally {
|
716
|
+
suggestDateBtn.disabled = false;
|
717
|
+
suggestDateBtn.textContent = "Suggest with AI";
|
718
|
+
}
|
719
|
+
});
|
720
|
+
}
|
721
|
+
};
|
722
|
+
|
723
|
+
// Render calendar view
|
724
|
+
const renderCalendar = () => {
|
725
|
+
// Get current month/year
|
726
|
+
const now = new Date();
|
727
|
+
const currentYear = now.getFullYear();
|
728
|
+
const currentMonth = now.getMonth();
|
729
|
+
|
730
|
+
// Get days in month
|
731
|
+
const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate();
|
732
|
+
|
733
|
+
// Get first day of month
|
734
|
+
const firstDayOfMonth = new Date(currentYear, currentMonth, 1).getDay();
|
735
|
+
|
736
|
+
// Create calendar grid
|
737
|
+
const days = [];
|
738
|
+
|
739
|
+
// Add empty cells for days before first day of month
|
740
|
+
for (let i = 0; i < firstDayOfMonth; i++) {
|
741
|
+
days.push(null);
|
742
|
+
}
|
743
|
+
|
744
|
+
// Add days of month
|
745
|
+
for (let i = 1; i <= daysInMonth; i++) {
|
746
|
+
days.push(i);
|
747
|
+
}
|
748
|
+
|
749
|
+
// Get posts for this month
|
750
|
+
const postsThisMonth = state.posts.filter((post) => {
|
751
|
+
if (!post.publish_date) return false;
|
752
|
+
const postDate = new Date(post.publish_date);
|
753
|
+
return (
|
754
|
+
postDate.getFullYear() === currentYear &&
|
755
|
+
postDate.getMonth() === currentMonth
|
756
|
+
);
|
757
|
+
});
|
758
|
+
|
759
|
+
// Group posts by day
|
760
|
+
const postsByDay = {};
|
761
|
+
|
762
|
+
postsThisMonth.forEach((post) => {
|
763
|
+
const postDate = new Date(post.publish_date);
|
764
|
+
const day = postDate.getDate();
|
765
|
+
|
766
|
+
if (!postsByDay[day]) {
|
767
|
+
postsByDay[day] = [];
|
768
|
+
}
|
769
|
+
|
770
|
+
postsByDay[day].push(post);
|
771
|
+
});
|
772
|
+
|
773
|
+
// Month names for header
|
774
|
+
const monthNames = [
|
775
|
+
"January",
|
776
|
+
"February",
|
777
|
+
"March",
|
778
|
+
"April",
|
779
|
+
"May",
|
780
|
+
"June",
|
781
|
+
"July",
|
782
|
+
"August",
|
783
|
+
"September",
|
784
|
+
"October",
|
785
|
+
"November",
|
786
|
+
"December",
|
787
|
+
];
|
788
|
+
|
789
|
+
// Render calendar
|
790
|
+
mainContent.innerHTML = `
|
791
|
+
<div class="card calendar">
|
792
|
+
<div class="calendar-header">
|
793
|
+
<h2 class="calendar-title">${
|
794
|
+
monthNames[currentMonth]
|
795
|
+
} ${currentYear}</h2>
|
796
|
+
<div class="calendar-navigation">
|
797
|
+
<button class="btn" id="prev-month-btn">Previous</button>
|
798
|
+
<button class="btn" id="next-month-btn">Next</button>
|
799
|
+
</div>
|
800
|
+
</div>
|
801
|
+
|
802
|
+
<div class="calendar-grid">
|
803
|
+
<div class="calendar-day-header">Sun</div>
|
804
|
+
<div class="calendar-day-header">Mon</div>
|
805
|
+
<div class="calendar-day-header">Tue</div>
|
806
|
+
<div class="calendar-day-header">Wed</div>
|
807
|
+
<div class="calendar-day-header">Thu</div>
|
808
|
+
<div class="calendar-day-header">Fri</div>
|
809
|
+
<div class="calendar-day-header">Sat</div>
|
810
|
+
|
811
|
+
${days
|
812
|
+
.map((day) => {
|
813
|
+
if (day === null) {
|
814
|
+
return `<div class="calendar-day" style="opacity: 0.2;"></div>`;
|
815
|
+
}
|
816
|
+
|
817
|
+
const isToday = day === now.getDate();
|
818
|
+
const dayPosts = postsByDay[day] || [];
|
819
|
+
|
820
|
+
return `
|
821
|
+
<div class="calendar-day ${
|
822
|
+
isToday ? "calendar-day-today" : ""
|
823
|
+
}" data-day="${day}">
|
824
|
+
<div class="calendar-day-number">${day}</div>
|
825
|
+
<div class="calendar-day-content">
|
826
|
+
${dayPosts
|
827
|
+
.map(
|
828
|
+
(post) => `
|
829
|
+
<div class="calendar-day-post" title="${
|
830
|
+
post.title || "Untitled"
|
831
|
+
}" data-post-id="${post.id}">
|
832
|
+
${post.title || "Untitled"}
|
833
|
+
</div>
|
834
|
+
`
|
835
|
+
)
|
836
|
+
.join("")}
|
837
|
+
${
|
838
|
+
dayPosts.length === 0
|
839
|
+
? ""
|
840
|
+
: `
|
841
|
+
<div class="calendar-day-count">${dayPosts.length} post${
|
842
|
+
dayPosts.length > 1 ? "s" : ""
|
843
|
+
}</div>
|
844
|
+
`
|
845
|
+
}
|
846
|
+
</div>
|
847
|
+
</div>
|
848
|
+
`;
|
849
|
+
})
|
850
|
+
.join("")}
|
851
|
+
</div>
|
852
|
+
</div>
|
853
|
+
`;
|
854
|
+
};
|
855
|
+
|
856
|
+
// Publish a post
|
857
|
+
const publishPost = async (postId) => {
|
858
|
+
try {
|
859
|
+
const button = document.querySelector(
|
860
|
+
`[data-action="publish-post"][data-post-id="${postId}"]`
|
861
|
+
);
|
862
|
+
|
863
|
+
if (button) {
|
864
|
+
button.disabled = true;
|
865
|
+
button.textContent = "Publishing...";
|
866
|
+
}
|
867
|
+
|
868
|
+
const response = await fetch(`/api/publish/${postId}`, {
|
869
|
+
method: "POST",
|
870
|
+
});
|
871
|
+
|
872
|
+
if (!response.ok) {
|
873
|
+
const error = await response.json();
|
874
|
+
throw new Error(error.error || "Failed to publish post");
|
875
|
+
}
|
876
|
+
|
877
|
+
const result = await response.json();
|
878
|
+
|
879
|
+
// Refresh posts
|
880
|
+
await fetchPosts();
|
881
|
+
|
882
|
+
// Update view
|
883
|
+
renderApp();
|
884
|
+
|
885
|
+
// Show success message
|
886
|
+
alert("Post published successfully!");
|
887
|
+
} catch (error) {
|
888
|
+
console.error("Error publishing post:", error);
|
889
|
+
alert(`Error: ${error.message}`);
|
890
|
+
|
891
|
+
// Re-enable button
|
892
|
+
const button = document.querySelector(
|
893
|
+
`[data-action="publish-post"][data-post-id="${postId}"]`
|
894
|
+
);
|
895
|
+
if (button) {
|
896
|
+
button.disabled = false;
|
897
|
+
button.textContent = "Publish";
|
898
|
+
}
|
899
|
+
}
|
900
|
+
};
|
901
|
+
|
902
|
+
// Format date for display
|
903
|
+
const formatDate = (dateStr) => {
|
904
|
+
if (!dateStr) return "No date set";
|
905
|
+
|
906
|
+
const date = new Date(dateStr);
|
907
|
+
const now = new Date();
|
908
|
+
const tomorrow = new Date(now);
|
909
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
910
|
+
|
911
|
+
// Check if date is today or tomorrow
|
912
|
+
if (date.toDateString() === now.toDateString()) {
|
913
|
+
return "Today";
|
914
|
+
} else if (date.toDateString() === tomorrow.toDateString()) {
|
915
|
+
return "Tomorrow";
|
916
|
+
}
|
917
|
+
|
918
|
+
// Format as Month Day, Year
|
919
|
+
const options = { month: "short", day: "numeric", year: "numeric" };
|
920
|
+
return date.toLocaleDateString(undefined, options);
|
921
|
+
};
|
922
|
+
|
923
|
+
// Format platforms for display
|
924
|
+
const formatPlatforms = (platforms) => {
|
925
|
+
if (!platforms) return "";
|
926
|
+
|
927
|
+
const platformsList = platforms.split(",").map((p) => p.trim());
|
928
|
+
|
929
|
+
return platformsList
|
930
|
+
.map((platform) => {
|
931
|
+
let icon = "";
|
932
|
+
|
933
|
+
switch (platform.toLowerCase()) {
|
934
|
+
case "twitter":
|
935
|
+
icon = "T";
|
936
|
+
break;
|
937
|
+
case "bluesky":
|
938
|
+
icon = "B";
|
939
|
+
break;
|
940
|
+
case "tiktok":
|
941
|
+
icon = "TT";
|
942
|
+
break;
|
943
|
+
default:
|
944
|
+
icon = platform.charAt(0).toUpperCase();
|
945
|
+
}
|
946
|
+
|
947
|
+
return `<div class="platform-icon" title="${platform}">${icon}</div>`;
|
948
|
+
})
|
949
|
+
.join("");
|
950
|
+
};
|
951
|
+
|
952
|
+
// Initialize the application
|
953
|
+
document.addEventListener("DOMContentLoaded", init);
|