From b57072f22f67932b31f31c33ddd9b932a85e9191 Mon Sep 17 00:00:00 2001 From: myeeli Date: Thu, 12 Jun 2025 18:33:56 -0400 Subject: [PATCH 001/174] Manvitha-Linkedin-Autoposter --- src/components/Announcements/index.jsx | 668 +++++++++++++++++++++---- 1 file changed, 564 insertions(+), 104 deletions(-) diff --git a/src/components/Announcements/index.jsx b/src/components/Announcements/index.jsx index a43a4e81ba..9c44a9f5cc 100644 --- a/src/components/Announcements/index.jsx +++ b/src/components/Announcements/index.jsx @@ -1,39 +1,50 @@ -/* eslint-disable no-undef */ +/* eslint-disable */ import { useState, useEffect } from 'react'; import './Announcements.css'; import { useDispatch, useSelector } from 'react-redux'; -import { Editor } from '@tinymce/tinymce-react'; +import axios from 'axios'; +import { Editor } from '@tinymce/tinymce-react'; // Import Editor from TinyMCE +import { sendEmail, broadcastEmailsToAll } from '../../actions/sendEmails'; import { boxStyle, boxStyleDark } from 'styles'; import { toast } from 'react-toastify'; -import { sendEmail, broadcastEmailsToAll } from '../../actions/sendEmails'; +import { Upload, Calendar, X, Loader, Edit, Trash2 } from 'lucide-react'; + -function Announcements({ title, email: initialEmail }) { +function Announcements() { const darkMode = useSelector(state => state.theme.darkMode); const dispatch = useDispatch(); - const [emailTo, setEmailTo] = useState(''); const [emailList, setEmailList] = useState([]); const [emailContent, setEmailContent] = useState(''); const [headerContent, setHeaderContent] = useState(''); - const [showEditor, setShowEditor] = useState(true); - const [isFileUploaded, setIsFileUploaded] = useState(false); + + const [linkedinContent, setLinkedinContent] = useState(''); + const [mediaFiles, setMediaFiles] = useState([]); + const [previews, setPreviews] = useState([]); + const [scheduleDate, setScheduleDate] = useState(''); + const [scheduleTime, setScheduleTime] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const [showEditor, setShowEditor] = useState(true); // State to control rendering of the editor + const [scheduledPosts, setScheduledPosts] = useState([]); + const [editingJobId, setEditingJobId] = useState(null); useEffect(() => { + // Toggle the showEditor state to force re-render when dark mode changes setShowEditor(false); setTimeout(() => setShowEditor(true), 0); }, [darkMode]); const editorInit = { license_key: 'gpl', - selector: 'Editor#email-editor', + selector: 'textarea#open-source-plugins', height: 500, + menubar: false, plugins: [ 'advlist autolink lists link image paste', 'charmap print preview anchor help', 'searchreplace visualblocks code', 'insertdatetime media table paste wordcount', ], - menubar: false, - branding: false, image_title: true, automatic_uploads: true, file_picker_callback(cb) { @@ -41,15 +52,31 @@ function Announcements({ title, email: initialEmail }) { input.setAttribute('type', 'file'); input.setAttribute('accept', 'image/*'); - input.onchange = () => { - const file = input.files[0]; + /* + Note: In modern browsers input[type="file"] is functional without + even adding it to the DOM, but that might not be the case in some older + or quirky browsers like IE, so you might want to add it to the DOM + just in case, and visually hide it. And do not forget do remove it + once you do not need it anymore. + */ + + input.onchange = function() { + const file = this.files[0]; + const reader = new FileReader(); - reader.onload = () => { + reader.onload = function() { + /* + Note: Now we need to register the blob in TinyMCE's image blob + registry. In the next release this part hopefully won't be + necessary, as we are looking to handle it internally. + */ const id = `blobid${new Date().getTime()}`; - const { blobCache } = window.tinymce.activeEditor.editorUpload; + const { blobCache } = tinymce.activeEditor.editorUpload; const base64 = reader.result.split(',')[1]; const blobInfo = blobCache.create(id, file, base64); blobCache.add(blobInfo); + + /* call the callback and populate the Title field with the file name */ cb(blobInfo.blobUri(), { title: file.name }); }; reader.readAsDataURL(file); @@ -58,41 +85,24 @@ function Announcements({ title, email: initialEmail }) { input.click(); }, a11y_advanced_options: true, + menubar: 'file insert edit view format tools', toolbar: - 'undo redo | bold italic | blocks fontfamily fontsize | image alignleft aligncenter alignright | bullist numlist outdent indent | removeformat | help', + 'undo redo | formatselect | bold italic | blocks fontfamily fontsize | image \ + alignleft aligncenter alignright | \ + bullist numlist outdent indent | removeformat | help', skin: darkMode ? 'oxide-dark' : 'oxide', content_css: darkMode ? 'dark' : 'default', }; - useEffect(() => { - if (initialEmail) { - const trimmedEmail = initialEmail.trim(); - setEmailTo(initialEmail); - setEmailList(trimmedEmail.split(',')); - } - }, [initialEmail]); - const handleEmailListChange = e => { - const { value } = e.target; - setEmailTo(value); - setEmailList(value.split(',')); + const emails = e.target.value.split(','); + setEmailList(emails); }; const handleHeaderContentChange = e => { setHeaderContent(e.target.value); }; - const addHeaderToEmailContent = () => { - if (!headerContent) return; - const imageTag = `Header Image`; - const editor = window.tinymce.get('email-editor'); - if (editor) { - editor.insertContent(imageTag); - setEmailContent(editor.getContent()); - } - setHeaderContent(''); - }; - const convertImageToBase64 = (file, callback) => { const reader = new FileReader(); reader.onloadend = () => { @@ -103,11 +113,10 @@ function Announcements({ title, email: initialEmail }) { const addImageToEmailContent = e => { const imageFile = document.querySelector('input[type="file"]').files[0]; - setIsFileUploaded(true); convertImageToBase64(imageFile, base64Image => { const imageTag = `Header Image`; setHeaderContent(prevContent => `${imageTag}${prevContent}`); - const editor = window.tinymce.get('email-editor'); + const editor = tinymce.get('email-editor'); if (editor) { editor.insertContent(imageTag); setEmailContent(editor.getContent()); @@ -116,7 +125,19 @@ function Announcements({ title, email: initialEmail }) { e.target.value = ''; }; + const addHeaderToEmailContent = () => { + if (!headerContent) return; + const imageTag = `Header Image`; + const editor = tinymce.get('email-editor'); + if (editor) { + editor.insertContent(imageTag); + setEmailContent(editor.getContent()); + } + setHeaderContent(''); // Clear the input field after inserting the header + }; + const validateEmail = email => { + /* Add a regex pattern for email validation */ const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; return emailPattern.test(email); }; @@ -124,21 +145,11 @@ function Announcements({ title, email: initialEmail }) { const handleSendEmails = () => { const htmlContent = emailContent; - if (emailList.length === 0 || emailList.every(e => !e.trim())) { + if (emailList.length === 0 || emailList.every(email => !email.trim())) { toast.error('Error: Empty Email List. Please enter AT LEAST One email.'); return; } - if (!isFileUploaded) { - toast.error('Error: Please upload a file.'); - return; - } - - if (!isFileUploaded) { - toast.error('Error: Please upload a file.'); - return; - } - const invalidEmails = emailList.filter(email => !validateEmail(email.trim())); if (invalidEmails.length > 0) { @@ -146,9 +157,7 @@ function Announcements({ title, email: initialEmail }) { return; } - dispatch( - sendEmail(emailList.join(','), title ? 'Anniversary congrats' : 'Weekly update', htmlContent), - ); + dispatch(sendEmail(emailList.join(','), 'Weekly Update', htmlContent)); }; const handleBroadcastEmails = () => { @@ -160,12 +169,281 @@ function Announcements({ title, email: initialEmail }) { dispatch(broadcastEmailsToAll('Weekly Update', htmlContent)); }; + // Handle media upload + const handleMediaUpload = (e) => { + const files = Array.from(e.target.files); + if (files.length === 0) return; + + // LinkedIn limits + const MAX_IMAGES = 9; + const MAX_VIDEOS = 1; + const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB + const MAX_VIDEO_SIZE = 200 * 1024 * 1024; // 200MB + + const existingVideos = mediaFiles.filter(file => file.type.includes('video')).length; + const existingImages = mediaFiles.filter(file => file.type.includes('image')).length; + const newVideos = files.filter(file => file.type.includes('video')).length; + const newImages = files.filter(file => file.type.includes('image')).length; + + // Validate video limit + if (existingVideos + newVideos > MAX_VIDEOS) { + toast.error('Only one video file is allowed per post'); + return; + } + + // Validate image limit + if (existingImages + newImages > MAX_IMAGES) { + toast.error('Maximum 9 images allowed per post'); + return; + } + + // Validate mixed content + if (newVideos > 0 && (existingImages > 0 || newImages > 0)) { + toast.error('Cannot mix videos and images in the same post'); + return; + } + if (existingVideos > 0 && (newImages > 0)) { + toast.error('Cannot mix videos and images in the same post'); + return; + } + + // Validate file sizes + const validFiles = files.filter((file) => { + const maxSize = file.type.includes('video') ? MAX_VIDEO_SIZE : MAX_IMAGE_SIZE; + if (file.size > maxSize) { + toast.error(`File "${file.name}" exceeds size limit`); + return false; + } + return true; + }); + + // Generate previews for valid files + validFiles.forEach((file) => { + const reader = new FileReader(); + reader.onload = () => { + setPreviews((prev) => [ + ...prev, + { + id: Math.random().toString(36).substr(2, 9), + url: reader.result, + type: file.type, + }, + ]); + }; + reader.readAsDataURL(file); + }); + + setMediaFiles((prev) => [...prev, ...validFiles]); + }; + + // Remove media file + const removeMedia = (index) => { + setMediaFiles((prev) => prev.filter((_, i) => i !== index)); + setPreviews((prev) => prev.filter((_, i) => i !== index)); + }; + + const handlePostToLinkedIn = async () => { + // console.log('Button clicked', { + // content: linkedinContent, + // scheduleDate, + // scheduleTime, + // mediaFiles + // }); + + // Validate content + if (!linkedinContent.trim()) { + toast.error('Post content cannot be empty'); + return; + } + + // Validate schedule time if provided + if (scheduleDate && scheduleTime) { + const scheduledDateTime = new Date(`${scheduleDate}T${scheduleTime}`); + const now = new Date(); + + // console.log('Schedule validation:', { + // scheduledDateTime, + // now, + // isValid: scheduledDateTime > now + // }); + + if (scheduledDateTime <= now) { + toast.error('Schedule time must be in the future'); + return; + } + } + + setIsLoading(true); + try { + const formData = new FormData(); + formData.append('content', linkedinContent.trim()); + + // Add schedule time if provided + if (scheduleDate && scheduleTime) { + const scheduledDateTime = new Date(`${scheduleDate}T${scheduleTime}`); + formData.append('scheduleTime', scheduledDateTime.toISOString()); + } + + // Append media files + mediaFiles.forEach((file) => { + formData.append('media', file); + }); + + // console.log('Sending request to:', 'http://localhost:4500/api/postToLinkedIn'); + // console.log('Request data:', { + // content: formData.get('content'), + // scheduleTime: formData.get('scheduleTime'), + // mediaFiles: Array.from(formData.getAll('media')).map(f => f.name) + // }); + + const response = await axios.post( + 'http://localhost:4500/api/postToLinkedIn', + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + maxContentLength: Infinity, + maxBodyLength: Infinity + } + ); + + // console.log('Response:', response.data); + + + if (response.data.success) { + if (editingJobId) { + try { + await axios.delete(`http://localhost:4500/api/scheduledPosts/${editingJobId}`); + toast.success('Old post deleted after rescheduling'); + } catch (error) { + console.error('Failed to delete old post:', error); + toast.error('Failed to delete old post'); + } + setEditingJobId(null); // Reset after delete + } + if (scheduleDate && scheduleTime) { + toast.success(`Post scheduled for ${new Date(`${scheduleDate}T${scheduleTime}`).toLocaleString()}`); + fetchScheduledPosts(); // Refresh the scheduled posts list + } else { + toast.success('Posted successfully to LinkedIn'); + } + resetLinkedInForm(); + } else { + throw new Error(response.data.message || 'Failed to post to LinkedIn'); + } + } catch (error) { + console.error('LinkedIn post error:', { + message: error.message, + response: error.response?.data, + status: error.response?.status, + stack: error.stack + }); + toast.error(error.response?.data?.message || 'Failed to post to LinkedIn'); + } finally { + setIsLoading(false); + } + }; + + const isScheduleTimeValid = () => { + if (!scheduleDate || !scheduleTime) { + return true; + } + + const now = new Date(); + const scheduledDateTime = new Date(`${scheduleDate}T${scheduleTime}`); + + return scheduledDateTime > now; + }; + + const handleScheduleDateChange = (e) => { + const newDate = e.target.value; + setScheduleDate(newDate); + + if (newDate) { + const now = new Date(); + const twoMinutesFromNow = new Date(now.getTime() + 2 * 60 * 1000); + const hours = String(twoMinutesFromNow.getHours()).padStart(2, '0'); + const minutes = String(twoMinutesFromNow.getMinutes()).padStart(2, '0'); + setScheduleTime(`${hours}:${minutes}`); + } else { + setScheduleTime(''); + } + }; + + const handleScheduleTimeChange = (e) => { + const newTime = e.target.value; + const now = new Date(); + const scheduledDateTime = new Date(`${scheduleDate}T${newTime}`); + const twoMinutesFromNow = new Date(now.getTime() + 2 * 60 * 1000); + + if (scheduledDateTime < twoMinutesFromNow) { + const hours = String(twoMinutesFromNow.getHours()).padStart(2, '0'); + const minutes = String(twoMinutesFromNow.getMinutes()).padStart(2, '0'); + setScheduleTime(`${hours}:${minutes}`); + toast.info('Time automatically adjusted to 2 minutes from now'); + } else { + setScheduleTime(newTime); + } + }; + + // Reset LinkedIn form + const resetLinkedInForm = () => { + setLinkedinContent(''); + setMediaFiles([]); + setPreviews([]); + setScheduleDate(''); + setScheduleTime(''); + }; + + const fetchScheduledPosts = async () => { + try { + const response = await axios.get('http://localhost:4500/api/scheduledPosts'); + if (response.data.success) { + setScheduledPosts(response.data.scheduledPosts); + } else { + throw new Error(response.data.message); + } + } catch (error) { + console.error('Failed to fetch scheduled posts:', error); + toast.error('Failed to fetch scheduled posts'); + } + }; + + useEffect(() => { + fetchScheduledPosts(); + }, []); + + const handleCancelScheduledPost = async (postId) => { + if (!window.confirm('Are you sure you want to cancel this scheduled post?')) { + return; + } + + setIsCanceling(true); + try { + console.log('Canceling post:', postId); + const response = await axios.delete(`http://localhost:4500/api/scheduledPosts/${postId}`); + console.log('Cancel response:', response.data); + + toast.success('Post cancelled successfully'); + fetchScheduledPosts(); // Refresh the list + } catch (error) { + console.error('Failed to cancel post:', { + postId, + error: error.message, + response: error.response?.data + }); + toast.error(error.response?.data?.message || 'Failed to cancel scheduled post'); + } finally { + setIsCanceling(false); + } + }; + return (
- {title ?

{title}

:

Weekly Progress Editor

} - +

Weekly Progress Editor


{showEditor && ( { + onEditorChange={(content, editor) => { setEmailContent(content); }} /> )} - {title ? ( - '' - ) : ( -
-
-
- -
-
+ + + {/* LinkedIn Editor Section */} +
+
+

+ LinkedIn Post Editor +

+ + {/* LinkedIn Content Input */} +