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.
@@ -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">&times;</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);