diff --git a/src/components/Announcements/CharacterCounter.jsx b/src/components/Announcements/CharacterCounter.jsx new file mode 100644 index 0000000000..3ce225c36a --- /dev/null +++ b/src/components/Announcements/CharacterCounter.jsx @@ -0,0 +1,37 @@ +import React from 'react'; + +const CharacterCounter = ({ currentLength, maxLength }) => { + const isOverLimit = currentLength > maxLength; + const percentage = (currentLength / maxLength) * 100; + + const getColor = () => { + if (isOverLimit) return '#dc3545'; // Red + if (percentage > 90) return '#ffc107'; // Yellow/warning + return '#28a745'; // Green + }; + + return ( +
+ {currentLength} + / + {maxLength} + {isOverLimit && ( + + ⚠️ Exceeds limit by {currentLength - maxLength} characters! + + )} +
+ ); +}; + +export default CharacterCounter; diff --git a/src/components/Announcements/ConfirmationModal.jsx b/src/components/Announcements/ConfirmationModal.jsx new file mode 100644 index 0000000000..afb8cec58b --- /dev/null +++ b/src/components/Announcements/ConfirmationModal.jsx @@ -0,0 +1,71 @@ +import React, { useState } from 'react'; +import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; + +const ConfirmationModal = ({ + isOpen, + toggle, + onConfirm, + title = 'Confirm Action', + message = 'Are you sure you want to proceed?', + confirmText = 'Confirm', + cancelText = 'Cancel', + confirmColor = 'primary', + showDontShowAgain = false, + onDontShowAgainChange = null, +}) => { + const [dontShowAgain, setDontShowAgain] = useState(false); + + const handleConfirm = () => { + if (showDontShowAgain && dontShowAgain && onDontShowAgainChange) { + onDontShowAgainChange(true); + } + onConfirm(); + toggle(); + setDontShowAgain(false); // Reset for next time + }; + + const handleCancel = () => { + toggle(); + setDontShowAgain(false); // Reset checkbox + }; + + return ( + + {title} + +

{message}

+ {showDontShowAgain && ( +
+ +
+ )} +
+ + + + +
+ ); +}; + +export default ConfirmationModal; diff --git a/src/components/Announcements/SocialMediaComposer.jsx b/src/components/Announcements/SocialMediaComposer.jsx index 1a338736a3..6ba6ae8de2 100644 --- a/src/components/Announcements/SocialMediaComposer.jsx +++ b/src/components/Announcements/SocialMediaComposer.jsx @@ -1,652 +1,477 @@ -/* eslint-disable jsx-a11y/label-has-associated-control, no-console, no-alert */ -import React, { useState, useEffect, useRef } from 'react'; -import axios from 'axios'; +import React, { useState, useEffect } from 'react'; +import { toast } from 'react-toastify'; +import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; +import CharacterCounter from './CharacterCounter'; +import ConfirmationModal from './ConfirmationModal'; +import './SocialMediaComposer.module.css'; -// --- DEBOUNCE HOOK --- -function useDebounce(value, delay) { - const [debouncedValue, setDebouncedValue] = useState(value); - useEffect(() => { - const handler = setTimeout(() => { - setDebouncedValue(value); - }, delay); - return () => { - clearTimeout(handler); - }; - }, [value, delay]); - return debouncedValue; -} +const PREFS_KEY = 'mastodon_composer_prefs'; -// --- HTML PARSER --- -const parseContentForEdit = htmlContent => { - const imgTagMatch = htmlContent.match(/]*>/i); - if (imgTagMatch) { - const imgTag = imgTagMatch[0]; - const srcMatch = imgTag.match(/src="([^"]+)"/i); - const altMatch = imgTag.match(/alt="([^"]*)"/i); - const imageSrc = srcMatch ? srcMatch[1] : null; - const altText = altMatch ? altMatch[1] : ''; - const textContent = htmlContent - .replace(/]*>/gi, '') - .replace(//g, '\n') - .trim(); - return { hasImage: !!imageSrc, imageSrc, altText, textContent }; - } - return { - hasImage: false, - imageSrc: null, - altText: '', - textContent: htmlContent.replace(//g, '\n'), +export default function SocialMediaComposer({ platform }) { + const PLATFORM_CHAR_LIMITS = { + mastodon: 500, + x: 280, + facebook: 63206, + linkedin: 3000, + instagram: 2200, + threads: 500, }; -}; -// --- MODAL COMPONENTS --- -const DeleteModal = ({ show, onClose, onDelete, theme, buttonStyles }) => { - if (!show) return null; - return ( -
-
-

Delete Scheduled Post

-

Are you sure?

-
- - -
-
-
- ); -}; -const PostNowModal = ({ show, onClose, onPost, theme, buttonStyles }) => { - if (!show) return null; - return ( -
-
-

Post Now

-

Post this immediately?

-
- - -
-
-
- ); -}; + const charLimit = PLATFORM_CHAR_LIMITS[platform] || 500; -const ToastNotification = ({ show, message, type, theme }) => { - if (!show) return null; - return ( -
- {message} -
- ); -}; -export default function SocialMediaComposer({ platform, darkMode = false }) { - // --- STATE --- - const [localContentValue, setLocalContentValue] = useState(''); - const postContent = useDebounce(localContentValue, 300); + const [postContent, setPostContent] = useState(''); const [activeSubTab, setActiveSubTab] = useState('composer'); - const [ljUsername, setLjUsername] = useState(''); - const [ljPassword, setLjPassword] = useState(''); - const [ljSubject, setLjSubject] = useState(''); - const [ljSecurity, setLjSecurity] = useState('public'); - const [ljTags, setLjTags] = useState(''); + const [isPosting, setIsPosting] = useState(false); const [scheduleDate, setScheduleDate] = useState(''); const [scheduleTime, setScheduleTime] = useState(''); const [scheduledPosts, setScheduledPosts] = useState([]); - const [postHistory, setPostHistory] = useState([]); - const [isPosting, setIsPosting] = useState(false); - const [toast, setToast] = useState({ show: false, message: '', type: 'success' }); - const [selectedImage, setSelectedImage] = useState(null); - const [imagePreview, setImagePreview] = useState(null); + const [isLoadingScheduled, setIsLoadingScheduled] = useState(false); + const [uploadedImage, setUploadedImage] = useState(null); const [imageAltText, setImageAltText] = useState(''); - const [showCrossPostDropdown, setShowCrossPostDropdown] = useState(false); - const crossPostDropdownRef = useRef(null); + const [postHistory, setPostHistory] = useState([]); + const [isLoadingHistory, setIsLoadingHistory] = useState(false); const [crossPostPlatforms, setCrossPostPlatforms] = useState({ facebook: false, - instagram: false, linkedin: false, - pinterest: false, + instagram: false, + x: false, }); - const [showDeleteModal, setShowDeleteModal] = useState(false); - const [showPostNowModal, setShowPostNowModal] = useState(false); - const [showEditModal, setShowEditModal] = useState(false); - const [selectedPostId, setSelectedPostId] = useState(null); - const [editingPost, setEditingPost] = useState(null); - const [editImageData, setEditImageData] = useState({ - hasImage: false, - imageSrc: null, - altText: '', + const [showCrossPost, setShowCrossPost] = useState(false); + const [editingPostId, setEditingPostId] = useState(null); + + const [modalOpen, setModalOpen] = useState(false); + const [modalConfig, setModalConfig] = useState({ + title: '', + message: '', + onConfirm: () => {}, + confirmText: 'Confirm', + confirmColor: 'primary', + showDontShowAgain: false, + preferenceKey: null, }); - const isLiveJournal = platform?.toLowerCase() === 'livejournal'; - const charLimit = 60000; - const charCount = localContentValue.length; - - // --- THEME DEFINITION --- - const theme = { - bg: darkMode ? '#121212' : '#ffffff', - text: darkMode ? '#e0e0e0' : '#212529', - subText: darkMode ? '#aaaaaa' : '#6c757d', - border: darkMode ? '#444444' : '#dee2e6', - inputBg: darkMode ? '#2d2d2d' : '#ffffff', - panelBg: darkMode ? '#1e1e1e' : '#f8f9fa', - modalBg: darkMode ? '#252525' : '#ffffff', - tabActiveBg: darkMode ? '#0d6efd' : '#e7f1ff', - tabActiveText: darkMode ? '#ffffff' : '#0d6efd', - tabInactiveBg: darkMode ? '#2d2d2d' : '#f8f9fa', - tabInactiveText: darkMode ? '#aaaaaa' : '#495057', - success: '#28a745', - danger: '#dc3545', - primary: '#0d6efd', + const [previewOpen, setPreviewOpen] = useState(false); + const [previewData, setPreviewData] = useState(null); + + const [preferences, setPreferences] = useState(() => { + const saved = localStorage.getItem(PREFS_KEY); + return saved + ? JSON.parse(saved) + : { + confirmDeleteScheduled: true, + confirmPostNow: true, + }; + }); + + const tabOrder = [ + { id: 'composer', label: '📝 Make Post' }, + { id: 'scheduled', label: '⏰ Scheduled Post' }, + { id: 'history', label: '📜 Post History' }, + { id: 'details', label: '🧩 Details' }, + ]; + + useEffect(() => { + if (activeSubTab === 'scheduled' && platform === 'mastodon') { + loadScheduledPosts(); + } else if (activeSubTab === 'history' && platform === 'mastodon') { + loadPostHistory(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeSubTab, platform]); + + const updatePreference = (key, value) => { + const newPrefs = { ...preferences, [key]: value }; + setPreferences(newPrefs); + localStorage.setItem(PREFS_KEY, JSON.stringify(newPrefs)); }; - // --- EFFECTS --- const loadScheduledPosts = async () => { + setIsLoadingScheduled(true); try { - const response = await axios.get('/api/livejournal/scheduled'); - if (response.data.success) setScheduledPosts(response.data.posts || []); - } catch (error) { - console.error(error); + const response = await fetch('/api/mastodon/schedule'); + if (response.ok) { + const data = await response.json(); + setScheduledPosts(data || []); + } + } catch (err) { + if (process.env.NODE_ENV === 'development') { + console.error('Error loading scheduled posts:', err); + } + } finally { + setIsLoadingScheduled(false); } }; + const loadPostHistory = async () => { + setIsLoadingHistory(true); try { - const response = await axios.get('/api/livejournal/history'); - if (response.data.success) setPostHistory(response.data.posts || []); - } catch (error) { - console.error(error); + const response = await fetch('/api/mastodon/history?limit=20'); + if (response.ok) { + const data = await response.json(); + setPostHistory(data || []); + } else { + toast.error('Failed to load post history'); + } + } catch (err) { + if (process.env.NODE_ENV === 'development') { + console.error('Error loading post history:', err); + } + toast.error('Error loading post history'); + } finally { + setIsLoadingHistory(false); } }; - useEffect(() => { - if (isLiveJournal) { - const savedUsername = localStorage.getItem('lj_username'); - if (savedUsername) setLjUsername(savedUsername); - loadScheduledPosts(); - loadPostHistory(); + const showModal = config => { + setModalConfig(config); + setModalOpen(true); + }; + + const handleImageUpload = e => { + const file = e.target.files[0]; + if (!file) return; + + if (!file.type.startsWith('image/')) { + toast.error('Please upload an image file'); + return; } - }, [isLiveJournal]); - useEffect(() => { - function handleClickOutside(event) { - if (crossPostDropdownRef.current && !crossPostDropdownRef.current.contains(event.target)) { - setShowCrossPostDropdown(false); - } + if (file.size > 5 * 1024 * 1024) { + toast.error('Image size must be less than 5MB'); + return; } - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, []); - - // --- HANDLERS --- - const showToast = (message, type = 'success') => { - setToast({ show: true, message, type }); - setTimeout(() => setToast({ show: false, message: '', type: 'success' }), 3000); + + const reader = new FileReader(); + reader.onloadend = () => { + const base64String = reader.result.split(',')[1]; + setUploadedImage({ + base64: base64String, + preview: reader.result, + name: file.name, + }); + toast.success('Image uploaded successfully!'); + }; + reader.onerror = () => { + toast.error('Failed to read image file'); + }; + reader.readAsDataURL(file); }; - const handleImageSelect = e => { - const file = e.target.files[0]; - if (file) { - if (file.size > 5 * 1024 * 1024) { - showToast('Image must be under 5MB', 'error'); - return; - } - setSelectedImage(file); - const reader = new FileReader(); - reader.onloadend = () => setImagePreview(reader.result); - reader.readAsDataURL(file); + + const handleRemoveImage = () => { + setUploadedImage(null); + setImageAltText(''); + const fileInput = document.getElementById('image-upload'); + if (fileInput) fileInput.value = ''; + }; + + const handleCrossPostToggle = platformName => { + setCrossPostPlatforms(prev => ({ + ...prev, + [platformName]: !prev[platformName], + })); + }; + + const handleShowPreview = () => { + if (!postContent.trim()) { + toast.error('Post cannot be empty!'); + return; } + + const selectedPlatforms = Object.keys(crossPostPlatforms).filter(p => crossPostPlatforms[p]); + + setPreviewData({ + content: postContent, + image: uploadedImage, + altText: imageAltText, + scheduledTime: scheduleDate && scheduleTime ? `${scheduleDate}T${scheduleTime}` : null, + crossPostTo: selectedPlatforms, + }); + setPreviewOpen(true); }; - const removeImage = () => { - setSelectedImage(null); - setImagePreview(null); + + const clearComposer = () => { + setPostContent(''); + setScheduleDate(''); + setScheduleTime(''); + setUploadedImage(null); setImageAltText(''); - }; - const handleCrossPostToggle = key => { - setCrossPostPlatforms(prev => ({ ...prev, [key]: !prev[key] })); + setCrossPostPlatforms({ facebook: false, linkedin: false, instagram: false, x: false }); + setEditingPostId(null); + setPreviewOpen(false); }; - const handlePost = async () => { - if (!isLiveJournal) return; - if (!ljUsername || !ljPassword) { - showToast('Please enter credentials', 'error'); + const handlePostNow = async () => { + if (!postContent.trim()) { + toast.error('Post cannot be empty!'); return; } - if (!postContent.trim() && !selectedImage) { - showToast('Content cannot be empty', 'error'); + if (postContent.length > charLimit) { + toast.error(`Post exceeds ${charLimit} character limit.`); return; } + + const selectedPlatforms = Object.keys(crossPostPlatforms).filter(p => crossPostPlatforms[p]); + setIsPosting(true); try { - const formData = new FormData(); - formData.append('username', ljUsername); - formData.append('password', ljPassword); - formData.append('subject', ljSubject || 'Untitled'); - formData.append('content', postContent); - formData.append('security', ljSecurity); - formData.append('tags', ljTags); - if (selectedImage) { - formData.append('image', selectedImage); - formData.append('altText', imageAltText); - } - const response = await axios.post('/api/livejournal/post', formData, { - headers: { 'Content-Type': 'multipart/form-data' }, + const response = await fetch('/api/mastodon/createPin', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: 'Mastodon Post', + description: postContent.trim(), + imgType: uploadedImage ? 'FILE' : 'URL', + mediaItems: uploadedImage ? `data:image/png;base64,${uploadedImage.base64}` : '', + mediaAltText: imageAltText || null, + crossPostTo: selectedPlatforms, + }), }); - if (response.data.success) { - showToast('Posted successfully!', 'success'); - setLocalContentValue(''); - setLjSubject(''); - setLjTags(''); - removeImage(); - loadPostHistory(); - setCrossPostPlatforms({ - facebook: false, - instagram: false, - linkedin: false, - pinterest: false, - }); + + if (response.ok) { + let message = `Successfully posted to ${platform}!`; + if (selectedPlatforms.length > 0) { + message += ` (Selected for: ${selectedPlatforms.join(', ')})`; + } + toast.success(message, { autoClose: 5000 }); + clearComposer(); + if (activeSubTab === 'history') { + loadPostHistory(); + } } else { - showToast(response.data.message || 'Failed', 'error'); + toast.error(`Failed to post to ${platform}.`); } - } catch (error) { - showToast('Error posting', 'error'); + } catch (err) { + toast.error(`Error while posting to ${platform}.`); } finally { setIsPosting(false); } }; - const handleSchedule = async () => { - if (!ljUsername || !ljPassword || !scheduleDate || !scheduleTime) { - showToast('Missing fields', 'error'); + const handleSchedulePost = async () => { + if (!postContent.trim()) { + toast.error('Post cannot be empty!'); return; } + if (postContent.length > charLimit) { + toast.error(`Post exceeds ${charLimit} character limit.`); + return; + } + if (!scheduleDate || !scheduleTime) { + toast.error('Please select both date and time.'); + return; + } + const scheduledDateTime = new Date(`${scheduleDate}T${scheduleTime}`); if (scheduledDateTime <= new Date()) { - showToast('Time must be in future', 'error'); + toast.error('Scheduled time must be in the future.'); return; } + + const selectedPlatforms = Object.keys(crossPostPlatforms).filter(p => crossPostPlatforms[p]); + setIsPosting(true); try { - const formData = new FormData(); - formData.append('username', ljUsername); - formData.append('password', ljPassword); - formData.append('subject', ljSubject || 'Untitled'); - formData.append('content', postContent); - formData.append('security', ljSecurity); - formData.append('tags', ljTags); - formData.append('scheduledDateTime', scheduledDateTime.toISOString()); - if (selectedImage) { - formData.append('image', selectedImage); - formData.append('altText', imageAltText); + // If editing, delete the old version first + if (editingPostId) { + await fetch(`/api/mastodon/schedule/${editingPostId}`, { method: 'DELETE' }); } - const response = await axios.post('/api/livejournal/schedule', formData, { - headers: { 'Content-Type': 'multipart/form-data' }, + + const response = await fetch('/api/mastodon/schedule', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: 'Mastodon Scheduled Post', + description: postContent.trim(), + imgType: uploadedImage ? 'FILE' : 'URL', + mediaItems: uploadedImage ? `data:image/png;base64,${uploadedImage.base64}` : '', + mediaAltText: imageAltText || null, + scheduledTime: scheduledDateTime.toISOString(), + crossPostTo: selectedPlatforms, + }), }); - if (response.data.success) { - showToast('Scheduled!', 'success'); - setLocalContentValue(''); - setLjSubject(''); - setLjTags(''); - setScheduleDate(''); - setScheduleTime(''); - removeImage(); - loadScheduledPosts(); + + if (response.ok) { + toast.success( + editingPostId ? 'Post updated successfully!' : 'Post scheduled successfully!', + ); + clearComposer(); + if (activeSubTab === 'scheduled') { + loadScheduledPosts(); + } + } else { + toast.error('Failed to schedule post.'); } - } catch (error) { - showToast('Error scheduling', 'error'); + } catch (err) { + toast.error('Error while scheduling post.'); } finally { setIsPosting(false); } }; - const openEditModal = post => { - const parsed = parseContentForEdit(post.content); - setEditingPost({ - ...post, - _id: post._id, - content: parsed.textContent, - scheduledFor: new Date(post.scheduledFor), - }); - setEditImageData({ - hasImage: parsed.hasImage, - imageSrc: parsed.imageSrc, - altText: parsed.altText, - }); - setShowEditModal(true); + const handleEditScheduled = post => { + try { + const postData = JSON.parse(post.postData); + + // Load post content + setPostContent(postData.status || ''); + + // Load image if exists + if (postData.local_media_base64) { + setUploadedImage({ + base64: postData.local_media_base64.replace(/^data:image\/\w+;base64,/, ''), + preview: postData.local_media_base64, + name: 'scheduled-image.png', + }); + } + + // Load alt text if exists + setImageAltText(postData.mediaAltText || ''); + + // Load scheduled time + const scheduledTime = new Date(post.scheduledTime); + const dateStr = scheduledTime.toISOString().split('T')[0]; + const timeStr = scheduledTime.toTimeString().slice(0, 5); + setScheduleDate(dateStr); + setScheduleTime(timeStr); + + // Set editing mode + setEditingPostId(post._id); + + // Switch to composer tab + setActiveSubTab('composer'); + + toast.info('Editing scheduled post. Modify and click "Schedule Post" to update.'); + } catch (err) { + toast.error('Failed to load post for editing'); + console.error('Edit error:', err); + } }; - const saveEdit = async () => { - try { - let finalContent = editingPost.content.replace(/\n/g, '
'); - if (editImageData.hasImage && editImageData.imageSrc) { - const alt = editImageData.altText ? editImageData.altText.replace(/"/g, '"') : ''; - const imgHtml = `${alt}`; - finalContent = finalContent ? `${imgHtml}

${finalContent}` : imgHtml; + const handleCancelEdit = () => { + clearComposer(); + toast.info('Edit cancelled'); + }; + + const handleDeleteScheduled = async (postId, skipConfirmation = false) => { + const performDelete = async () => { + try { + const response = await fetch(`/api/mastodon/schedule/${postId}`, { + method: 'DELETE', + }); + if (response.ok) { + toast.success('Scheduled post deleted!'); + loadScheduledPosts(); + } else { + toast.error('Failed to delete post.'); + } + } catch (err) { + toast.error('Error deleting post.'); } - await axios.put(`/api/livejournal/schedule/${editingPost._id}`, { - subject: editingPost.subject, - content: finalContent, - security: editingPost.security, - tags: editingPost.tags, - scheduledDateTime: editingPost.scheduledFor.toISOString(), + }; + + if (skipConfirmation || !preferences.confirmDeleteScheduled) { + await performDelete(); + } else { + showModal({ + title: 'Delete Scheduled Post', + message: 'Are you sure you want to delete this scheduled post?', + onConfirm: performDelete, + confirmText: 'Delete', + confirmColor: 'danger', + showDontShowAgain: true, + preferenceKey: 'confirmDeleteScheduled', }); - showToast('Updated!', 'success'); - loadScheduledPosts(); - setShowEditModal(false); - } catch (error) { - console.error(error); - showToast('Error updating', 'error'); } }; - const formatDate = d => - new Date(d).toLocaleString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: 'numeric', - minute: '2-digit', - hour12: true, - }); + const handlePostScheduledNow = async post => { + const performPost = async () => { + try { + const postData = JSON.parse(post.postData); + const response = await fetch('/api/mastodon/createPin', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: 'Mastodon Post', + description: postData.status, + imgType: postData.local_media_base64 ? 'FILE' : 'URL', + mediaItems: postData.local_media_base64 || '', + mediaAltText: postData.mediaAltText || null, + }), + }); - // --- COMMON STYLES --- - const inputStyle = { - width: '100%', - padding: '10px', - borderRadius: '6px', - border: `1px solid ${theme.border}`, - backgroundColor: theme.inputBg, - color: theme.text, + if (response.ok) { + toast.success('Posted successfully!'); + await handleDeleteScheduled(post._id, true); + } else { + toast.error('Failed to post.'); + } + } catch (err) { + toast.error('Error posting.'); + } + }; + + if (!preferences.confirmPostNow) { + await performPost(); + } else { + showModal({ + title: 'Post Immediately', + message: + 'This will post immediately to Mastodon and remove it from your scheduled posts. Continue?', + onConfirm: performPost, + confirmText: 'Post Now', + confirmColor: 'success', + showDontShowAgain: true, + preferenceKey: 'confirmPostNow', + }); + } }; - const labelStyle = { - display: 'block', - marginBottom: '8px', - fontWeight: '500', - color: theme.text, + + const handleDontShowAgainChange = preferenceKey => { + updatePreference(preferenceKey, false); + toast.info('Preference saved! This confirmation will not show again.', { autoClose: 3000 }); }; - const buttonBase = { - padding: '12px 24px', - border: 'none', - borderRadius: '4px', - cursor: 'pointer', - fontWeight: '500', + + const formatScheduledTime = isoString => { + try { + return new Date(isoString).toLocaleString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: 'numeric', + minute: '2-digit', + hour12: true, + }); + } catch { + return isoString; + } }; - const primaryButton = { ...buttonBase, backgroundColor: theme.primary, color: '#ffffff' }; - const successButton = { ...buttonBase, backgroundColor: theme.success, color: '#ffffff' }; - const dangerButton = { - ...buttonBase, - backgroundColor: theme.danger, - color: '#ffffff', - padding: '6px 12px', - fontSize: '14px', + + const getScheduledPostImage = post => { + try { + const postData = JSON.parse(post.postData); + return postData.local_media_base64 || null; + } catch { + return null; + } }; - const secondaryButton = { - ...buttonBase, - backgroundColor: theme.inputBg, - color: theme.text, - border: `1px solid ${theme.border}`, - padding: '8px 16px', + + const stripHtml = html => { + const tmp = document.createElement('DIV'); + tmp.innerHTML = html; + return tmp.textContent || tmp.innerText || ''; }; return ( -
- - setShowDeleteModal(false)} - onDelete={async () => { - try { - await axios.delete(`/api/livejournal/schedule/${selectedPostId}`); - showToast('Deleted!', 'success'); - loadScheduledPosts(); - } catch { - showToast('Error', 'error'); - } finally { - setShowDeleteModal(false); - } - }} - theme={theme} - buttonStyles={{ secondary: secondaryButton, danger: dangerButton }} - /> - - setShowPostNowModal(false)} - onPost={async () => { - try { - await axios.post(`/api/livejournal/post-scheduled/${selectedPostId}`); - showToast('Posted!', 'success'); - loadScheduledPosts(); - loadPostHistory(); - } catch { - showToast('Error', 'error'); - } finally { - setShowPostNowModal(false); - } - }} - theme={theme} - buttonStyles={{ secondary: secondaryButton, success: successButton }} - /> +
+

{platform}

- {showEditModal && editingPost && ( -
-
-

Edit Post

-
- - setEditingPost(prev => ({ ...prev, subject: e.target.value }))} - style={inputStyle} - /> -
-
- - {editImageData.hasImage && ( -
- {editImageData.altText} -
- - - setEditImageData(prev => ({ ...prev, altText: e.target.value })) - } - style={{ ...inputStyle, padding: '6px', fontSize: '13px' }} - /> -
- -
- )} -