diff --git a/src/components/Announcements/Announcements.module.css b/src/components/Announcements/Announcements.module.css index d47d71a7a3..f937e535c9 100644 --- a/src/components/Announcements/Announcements.module.css +++ b/src/components/Announcements/Announcements.module.css @@ -209,6 +209,38 @@ button.sendButton:hover { display: block; } +.tabIconWrapper { + position: relative; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto; +} + +.scheduleBadge { + position: absolute; + top: -6px; + right: -10px; + background: #f55151; + color: #fff; + border-radius: 999px; + font-size: 0.65rem; + font-weight: 700; + padding: 2px 6px; + min-width: 22px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 2px solid #fff; + line-height: 1; +} + +.tabNavItem.dark .scheduleBadge { + border-color: #14233a; +} + .tabLabel { font-size: 0.75rem; text-align: center; diff --git a/src/components/Announcements/index.jsx b/src/components/Announcements/index.jsx index 7c92fe3445..383bf16abf 100644 --- a/src/components/Announcements/index.jsx +++ b/src/components/Announcements/index.jsx @@ -16,6 +16,8 @@ import { import { faFacebook, faLinkedin, faMedium } from '@fortawesome/free-brands-svg-icons'; import ReactTooltip from 'react-tooltip'; import EmailPanel from './platforms/email'; +import MyspaceAutoPoster from './platforms/myspace'; +import PlatformScheduleBadge from './platforms/PlatformScheduleBadge'; function Announcements({ title, email: initialEmail }) { const [activeTab, setActiveTab] = useState('email'); @@ -104,15 +106,19 @@ function Announcements({ title, email: initialEmail }) { onClick={() => setActiveTab(id)} aria-selected={activeTab === id} > -
+
{customIconSrc ? ( {`${label} ) : ( )} + {id === 'myspace' && ( + + )}
{label}
@@ -165,11 +171,15 @@ function Announcements({ title, email: initialEmail }) { 'slashdot', 'blogger', 'truthsocial', - ].map(platform => ( - - - - ))} + ].map(platform => { + const PlatformComposer = + platform === 'myspace' ? MyspaceAutoPoster : SocialMediaComposer; + return ( + + + + ); + })}
diff --git a/src/components/Announcements/platforms/PlatformScheduleBadge.jsx b/src/components/Announcements/platforms/PlatformScheduleBadge.jsx new file mode 100644 index 0000000000..cd25082064 --- /dev/null +++ b/src/components/Announcements/platforms/PlatformScheduleBadge.jsx @@ -0,0 +1,59 @@ +import { useEffect, useState } from 'react'; + +const scheduleCounts = new Map(); +const listeners = new Map(); + +const getScheduleCount = platform => scheduleCounts.get(platform) || 0; + +const notify = (platform, count) => { + const platformListeners = listeners.get(platform); + if (!platformListeners) return; + platformListeners.forEach(listener => { + try { + listener(count); + } catch (error) { + // eslint-disable-next-line no-console + console.error('PlatformScheduleBadge listener error', error); + } + }); +}; + +export const setPlatformScheduleCount = (platform, count) => { + if (!platform) return; + const normalizedCount = Math.max(0, Number.isFinite(count) ? count : 0); + const nextCount = Math.round(normalizedCount); + scheduleCounts.set(platform, nextCount); + notify(platform, nextCount); +}; + +const subscribeToScheduleCount = (platform, listener) => { + if (!listeners.has(platform)) listeners.set(platform, new Set()); + const platformListeners = listeners.get(platform); + platformListeners.add(listener); + return () => { + platformListeners.delete(listener); + if (platformListeners.size === 0) { + listeners.delete(platform); + } + }; +}; + +export const usePlatformScheduleCount = platform => { + const [count, setCount] = useState(() => getScheduleCount(platform)); + + useEffect(() => { + setCount(getScheduleCount(platform)); + return subscribeToScheduleCount(platform, setCount); + }, [platform]); + + return count; +}; + +const PlatformScheduleBadge = ({ platform, className }) => { + const count = usePlatformScheduleCount(platform); + if (!count) return null; + const displayValue = count > 99 ? '99+' : count; + return {displayValue}; +}; + +export default PlatformScheduleBadge; diff --git a/src/components/Announcements/platforms/myspace/Myspace.module.css b/src/components/Announcements/platforms/myspace/Myspace.module.css new file mode 100644 index 0000000000..664078e6dd --- /dev/null +++ b/src/components/Announcements/platforms/myspace/Myspace.module.css @@ -0,0 +1,484 @@ + + +.myspace-autoposter { + /* max-width: 980px; */ + width: 100%; + margin: 0 auto; + display: grid; + gap: 24px; +} + +.myspace-autoposter.dark { + color: #dbe6ff; +} + +.myspace-autoposter.dark label { + color: #dbe6ff; +} + +.myspace-autoposter.dark p { + color: #dbe6ff; +} + +.myspace-subtabs { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 18px; + border-bottom: 1px solid #ccd4e0; +} + +.myspace-subtab { + padding: 9px 16px; + border-radius: 6px 6px 0 0; + border: 1px solid transparent; + border-bottom: none; + background: #d9d9d9; + color: #333; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.myspace-subtab:hover { + background: #cfcfcf; +} + +.myspace-subtab.active { + background: #d7ecff; + color: #0d6efd; + border-color: #99c8ff; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6); +} + +.myspace-autoposter.dark .myspace-subtab { + border-color: transparent; + background: #2d3c53; + color: #cdd8f6; +} + +.myspace-autoposter.dark .myspace-subtab.active { + background: #1f4a80; + border-color: #1f4a80; + color: #fff; +} + +.myspace-card { + background: #fff; + border: 1px solid #d6dde7; + border-radius: 12px; + padding: 20px 22px; + box-shadow: 0 10px 24px rgba(15, 37, 80, 0.08); + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.myspace-autoposter.dark .myspace-card { + background: #14233a; + border-color: #25354d; + box-shadow: none; +} + +.myspace-card.invalid { + border-color: #d9534f; + box-shadow: 0 0 0 1px rgba(217, 83, 79, 0.18); +} + +.myspace-autoposter.dark .myspace-card.invalid { + border-color: #ff7b72; + box-shadow: none; +} + +.myspace-grid { + display: grid; + gap: 20px; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); +} + +.myspace-field__header { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 12px; + margin-bottom: 8px; +} + +.myspace-field__meta { + font-size: 0.85rem; + color: #6c757d; +} + +.myspace-field__meta.invalid { + color: #d9534f; +} + +.myspace-autoposter.dark .myspace-field__meta { + color: #9aa9c6; +} + +.myspace-field__required { + color: #d9534f; + margin-left: 4px; +} + +.myspace-autoposter.dark .myspace-field__required { + color: #ff9384; +} + +.myspace-field__input { + width: 100%; + border: 1px solid #c7d1e5; + border-radius: 8px; + padding: 12px 14px; + font-size: 0.95rem; + background: #fff; + color: #1b1f29; +} + +.myspace-autoposter.dark .myspace-field__input { + background: #0f1c2d; + border-color: #2b3b55; + color: #e4edff; +} + +.myspace-field__input--invalid { + border-color: #d9534f; + box-shadow: 0 0 0 1px rgba(217, 83, 79, 0.2); +} + +.myspace-autoposter.dark .myspace-field__input--invalid { + border-color: #ff9384; + box-shadow: 0 0 0 1px rgba(255, 147, 132, 0.3); +} + +.myspace-field__textarea { + resize: vertical; + min-height: 110px; + white-space: pre-wrap; + overflow-wrap: anywhere; + word-break: break-word; +} + +.myspace-field__file { + display: none; +} + +.myspace-field__error { + color: #d9534f; + font-size: 0.85rem; + margin-top: 8px; +} + +.myspace-autoposter.dark .myspace-field__error { + color: #ff9384; +} + +.myspace-field__hint { + color: #6c757d; + font-size: 0.85rem; + margin-top: 8px; +} + +.myspace-autoposter.dark .myspace-field__hint { + color: #9aa9c6; +} + +.myspace-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 10px; +} + +.myspace-chip { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 4px 10px; + border-radius: 999px; + background: #e9efff; + color: #1c3f82; + font-size: 0.8rem; + font-weight: 600; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + gap: 6px; +} + +.myspace-autoposter.dark .myspace-chip { + background: rgba(13, 110, 253, 0.22); + color: #cfe0ff; +} + +.myspace-chip__label { + overflow: hidden; + text-overflow: ellipsis; +} + +.myspace-chip__clear { + border: none; + background: transparent; + cursor: pointer; + color: #d9534f; + font-size: 0.9rem; + padding: 0 4px; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + transition: color 0.2s ease; +} + +.myspace-chip__clear:hover, +.myspace-chip__clear:focus { + color: #b7322d; + background: transparent; +} + +.myspace-autoposter.dark .myspace-chip__clear { + color: #ff9384; +} + +.myspace-preview__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} + +.myspace-preview__actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: flex-end; +} + +.myspace-preview__body { + white-space: pre-wrap; + font-family: 'SFMono-Regular', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', + monospace; + background: #f8f9fb; + border: 1px solid #d6dde7; + border-radius: 8px; + padding: 16px; + color: #27324b; + max-height: 240px; + overflow: auto; + overflow-wrap: anywhere; + word-break: break-word; +} + +.myspace-autoposter.dark .myspace-preview__body { + background: #0f1c2d; + border-color: #2b3b55; + color: #e4edff; +} + +.myspace-preview__hint { + font-size: 0.85rem; + color: #6c757d; + margin-top: 12px; +} + +.myspace-autoposter.dark .myspace-preview__hint { + color: #9aa9c6; +} + +.myspace-meta__grid { + display: grid; + gap: 16px; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); +} + +.myspace-meta__column { + display: flex; + flex-direction: column; + gap: 10px; +} + +.myspace-field__sublabel { + display: block; + font-weight: 600; + margin-bottom: 4px; + color: #1b1f29; +} + +.myspace-autoposter.dark .myspace-field__sublabel { + color: #dbe6ff; +} + +.myspace-photo__meta { + font-size: 0.85rem; + color: #6c757d; + margin-top: 6px; +} + +.myspace-autoposter.dark .myspace-photo__meta { + color: #9aa9c6; +} + +.myspace-photo__preview { + margin-top: 10px; + border-radius: 8px; + border: 1px solid #d6dde7; + max-width: 100%; + max-height: 220px; + object-fit: cover; +} + +.myspace-autoposter.dark .myspace-photo__preview { + border-color: #2b3b55; +} + +.myspace-card--scheduler { + max-width: 720px; +} + +.myspace-scheduler__grid { + display: grid; + gap: 20px; + grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); + align-items: start; +} + +.myspace-card--saved { + max-width: 100%; +} + +.myspace-scheduler__note { + font-size: 0.85rem; + color: #6c757d; + margin-top: 12px; +} + +.myspace-autoposter.dark .myspace-scheduler__note { + color: #9aa9c6; +} + +.myspace-scheduler__controls { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin: 18px 0; +} + +.myspace-scheduler__field { + flex: 1 1 200px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.myspace-scheduler__textarea { + min-height: 220px; +} + +.myspace-scheduler__actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 18px; +} + +.myspace-scheduler__empty { + font-size: 0.9rem; + color: #6c757d; + margin: 8px 0 0; +} + +.myspace-autoposter.dark .myspace-scheduler__empty { + color: #9aa9c6; +} + +.myspace-saved__list { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 18px; +} + +.myspace-saved__item { + border: 1px solid #d6dde7; + border-radius: 10px; + background: #f4f7fd; + padding: 12px 14px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.myspace-autoposter.dark .myspace-saved__item { + border-color: #2b3b55; + background: #0f1c2d; +} + +.myspace-saved__item--active { + border-color: #0d6efd; + box-shadow: 0 0 0 1px rgba(13, 110, 253, 0.24); +} + +.myspace-autoposter.dark .myspace-saved__item--active { + border-color: #4785ff; + box-shadow: 0 0 0 1px rgba(71, 133, 255, 0.32); +} + +.myspace-saved__header { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 12px; +} + +.myspace-saved__title { + font-size: 1rem; + font-weight: 600; + margin: 0; + color: #1b1f29; +} + +.myspace-autoposter.dark .myspace-saved__title { + color: #dbe6ff; +} + +.myspace-saved__meta { + font-size: 0.85rem; + color: #6c757d; +} + +.myspace-autoposter.dark .myspace-saved__meta { + color: #9aa9c6; +} + +.myspace-saved__excerpt { + font-size: 0.9rem; + color: #4f5a73; + margin: 0; +} + +.myspace-autoposter.dark .myspace-saved__excerpt { + color: #cfd9f8; +} + +.myspace-saved__actions { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +@media (max-width: 960px) { + .myspace-scheduler__grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 640px) { + .myspace-autoposter { + gap: 18px; + } + + .myspace-card { + padding: 18px; + } +} diff --git a/src/components/Announcements/platforms/myspace/index.jsx b/src/components/Announcements/platforms/myspace/index.jsx new file mode 100644 index 0000000000..9c3e55a1b4 --- /dev/null +++ b/src/components/Announcements/platforms/myspace/index.jsx @@ -0,0 +1,1121 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { useSelector } from 'react-redux'; +import { toast } from 'react-toastify'; +import styles from './Myspace.module.css'; +import { setPlatformScheduleCount } from '../PlatformScheduleBadge'; + +const HEADLINE_MIN = 12; +const HEADLINE_MAX = 95; +const BODY_MIN = 80; +const STOP_WORDS = new Set([ + 'about', + 'after', + 'also', + 'another', + 'because', + 'been', + 'being', + 'between', + 'can', + 'could', + 'during', + 'each', + 'from', + 'have', + 'into', + 'more', + 'other', + 'over', + 'since', + 'some', + 'than', + 'that', + 'their', + 'there', + 'these', + 'they', + 'this', + 'through', + 'under', + 'until', + 'where', + 'which', + 'while', + 'with', + 'within', +]); + +const MYSPACE_SCHEDULE_STORAGE_KEY = 'hgn_myspace_schedules'; + +const readSchedulesFromStorage = () => { + if (typeof window === 'undefined' || !window?.localStorage) { + return { data: [], error: new Error('Local storage unavailable') }; + } + try { + const stored = window.localStorage.getItem(MYSPACE_SCHEDULE_STORAGE_KEY); + if (!stored) return { data: [] }; + const parsed = JSON.parse(stored); + if (!Array.isArray(parsed)) return { data: [] }; + return { data: parsed }; + } catch (error) { + return { data: [], error }; + } +}; + +const persistSchedulesToStorage = schedules => { + if (typeof window === 'undefined' || !window?.localStorage) { + return { success: false, error: new Error('Local storage unavailable') }; + } + try { + window.localStorage.setItem(MYSPACE_SCHEDULE_STORAGE_KEY, JSON.stringify(schedules)); + return { success: true }; + } catch (error) { + return { success: false, error }; + } +}; + +const sanitizeTags = text => + text + .split(',') + .map(tag => + tag + .trim() + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^a-z0-9-]/g, ''), + ) + .filter(Boolean); + +const extractTagCandidates = (headline, summary, existing) => { + if (Array.isArray(existing) && existing.length) return existing.slice(0, 6); + const corpus = `${headline} ${summary}`.toLowerCase(); + const words = corpus.match(/[a-z0-9']+/g) || []; + const candidates = []; + for (const raw of words) { + const cleaned = raw.replace(/'/g, ''); + if (cleaned.length < 4) continue; + if (STOP_WORDS.has(cleaned)) continue; + if (!candidates.includes(cleaned)) candidates.push(cleaned); + if (candidates.length === 6) break; + } + return candidates; +}; + +const buildPreview = ({ headline, body, sourceUrl, tags }) => + `${headline?.trim() || '—'}\n\n${body?.trim() || '—'}\n\n${sourceUrl?.trim() || '—'}\n\n${ + tags.length ? tags.join(', ') : '—' + }\n`; + +const padTimeUnit = value => String(value).padStart(2, '0'); + +const formatLocalDate = date => + `${date.getFullYear()}-${padTimeUnit(date.getMonth() + 1)}-${padTimeUnit(date.getDate())}`; + +const formatLocalTime = date => `${padTimeUnit(date.getHours())}:${padTimeUnit(date.getMinutes())}`; + +const generateRandomSlug = () => { + if (typeof window !== 'undefined' && window.crypto?.getRandomValues) { + const buffer = new Uint32Array(3); + window.crypto.getRandomValues(buffer); + return Array.from(buffer, value => value.toString(36).slice(0, 4)).join(''); + } + let fallback = ''; + for (let i = 0; i < 3; i += 1) { + fallback += Date.now().toString(36); + } + return fallback.slice(0, 12); +}; +const createScheduleId = () => `schedule-${Date.now().toString(36)}-${generateRandomSlug()}`; + +const normalizeScheduleRecord = record => { + if (!record || typeof record !== 'object') return null; + const normalizedId = record._id || record.id || createScheduleId(); + return { + ...record, + id: normalizedId, + _id: record._id || normalizedId, + updatedAt: record.updatedAt || record.updated_at || new Date().toISOString(), + }; +}; + +const sortSchedulesByUpdatedAt = schedules => + [...schedules].sort((a, b) => { + const getTime = entry => + new Date(entry?.updatedAt || entry?.createdAt || entry?.created_at || 0).getTime(); + return getTime(b) - getTime(a); + }); + +const formatDisplayDateTime = (dateString, timeString) => { + if (!dateString) return '—'; + try { + const composed = `${dateString}T${timeString || '00:00'}`; + const parsed = new Date(composed); + if (Number.isNaN(parsed.getTime())) { + return `${dateString}${timeString ? `, ${timeString}` : ''}`; + } + const formattedDate = parsed.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + const formattedTime = timeString + ? parsed.toLocaleTimeString(undefined, { + hour: '2-digit', + minute: '2-digit', + }) + : ''; + return formattedTime ? `${formattedDate} • ${formattedTime}` : formattedDate; + } catch (error) { + return `${dateString}${timeString ? `, ${timeString}` : ''}`; + } +}; + +const topCardActions = () => ({ + display: 'flex', + flexWrap: 'wrap', + gap: '12px', + marginTop: '16px', +}); + +const buttonStyle = (variant, darkMode) => { + const base = { + borderRadius: '999px', + border: 'none', + cursor: 'pointer', + fontWeight: 600, + padding: '10px 18px', + transition: 'filter 0.2s ease', + }; + if (variant === 'primary') { + return { + ...base, + backgroundColor: '#0d6efd', + color: '#fff', + }; + } + if (variant === 'outline') { + return { + ...base, + backgroundColor: 'transparent', + color: darkMode ? '#9bb5ff' : '#0d6efd', + border: `1px solid ${darkMode ? '#3d4d6d' : '#0d6efd'}`, + }; + } + return { + ...base, + backgroundColor: darkMode ? '#1c2b44' : '#e9efff', + color: darkMode ? '#cfd9f8' : '#1c3f82', + }; +}; + +const fieldActionRow = { + display: 'flex', + flexWrap: 'wrap', + gap: '10px', + marginTop: '12px', +}; + +function MyspaceAutoPoster({ platform }) { + const darkMode = useSelector(state => state.theme.darkMode); + + const [headline, setHeadline] = useState(''); + const [sourceUrl, setSourceUrl] = useState(''); + const [photoUrl, setPhotoUrl] = useState(''); + const [photoFileName, setPhotoFileName] = useState(''); + const [photoPreviewUrl, setPhotoPreviewUrl] = useState(''); + const [tagsText, setTagsText] = useState(''); + const [body, setBody] = useState(''); + const [song, setSong] = useState(''); + const [activeSubTab, setActiveSubTab] = useState('make'); + const [scheduledDraft, setScheduledDraft] = useState(''); + const [scheduledDate, setScheduledDate] = useState(() => formatLocalDate(new Date())); + const [scheduledTime, setScheduledTime] = useState(() => formatLocalTime(new Date())); + const [savedSchedules, setSavedSchedules] = useState([]); + const [schedulesLoading, setSchedulesLoading] = useState(false); + const [scheduleSyncError, setScheduleSyncError] = useState(''); + const [scheduleSaving, setScheduleSaving] = useState(false); + const [editingScheduleId, setEditingScheduleId] = useState(null); + const [scheduleAttemptedSave, setScheduleAttemptedSave] = useState(false); + const photoInputRef = useRef(null); + + useEffect(() => { + if (platform !== 'myspace') return undefined; + let isMounted = true; + + const loadSchedulesFromBrowser = () => { + if (!isMounted) return; + setSchedulesLoading(true); + setScheduleSyncError(''); + const { data, error } = readSchedulesFromStorage(); + if (error && isMounted) { + setSavedSchedules([]); + setScheduleSyncError('Unable to load saved scheduled posts from this browser.'); + toast.error('Unable to load saved Myspace posts. Local storage may be disabled.'); + } else if (isMounted) { + const normalized = (Array.isArray(data) ? data : []) + .map(normalizeScheduleRecord) + .filter(Boolean); + setSavedSchedules(sortSchedulesByUpdatedAt(normalized)); + } + if (isMounted) { + setSchedulesLoading(false); + } + }; + + loadSchedulesFromBrowser(); + + const handleStorageSync = event => { + if (event.key === MYSPACE_SCHEDULE_STORAGE_KEY) { + loadSchedulesFromBrowser(); + } + }; + + if (typeof window !== 'undefined') { + window.addEventListener('storage', handleStorageSync); + } + + return () => { + isMounted = false; + if (typeof window !== 'undefined') { + window.removeEventListener('storage', handleStorageSync); + } + }; + }, [platform]); + + useEffect(() => { + if (platform !== 'myspace') return undefined; + setPlatformScheduleCount(platform, savedSchedules.length); + return () => setPlatformScheduleCount(platform, 0); + }, [platform, savedSchedules.length]); + + useEffect(() => { + if (!photoPreviewUrl) return undefined; + return () => { + URL.revokeObjectURL(photoPreviewUrl); + }; + }, [photoPreviewUrl]); + + const subTabs = useMemo( + () => [ + { id: 'make', label: '📝 Make Post' }, + { id: 'schedule', label: '⏰ Scheduled Post' }, + ], + [], + ); + + const tags = useMemo(() => sanitizeTags(tagsText), [tagsText]); + + const trimmedHeadline = headline.trim(); + const trimmedUrl = sourceUrl.trim(); + const trimmedBody = body.trim(); + const trimmedPhoto = photoUrl.trim(); + const trimmedSong = song.trim(); + + const headlineInRange = + trimmedHeadline.length >= HEADLINE_MIN && trimmedHeadline.length <= HEADLINE_MAX; + const urlValid = /^https?:\/\//i.test(trimmedUrl); + const bodyValid = trimmedBody.length >= BODY_MIN; + const tagsValid = tags.length > 0; + + const readyToCopy = headlineInRange && bodyValid && tagsValid; + + const highlightHeadline = trimmedHeadline.length > 0 && !headlineInRange; + const highlightUrl = trimmedUrl.length > 0 && !urlValid; + const highlightBody = trimmedBody.length > 0 && !bodyValid; + + const hasAnyInput = Boolean( + trimmedHeadline || + trimmedUrl || + trimmedBody || + tagsText.trim() || + trimmedPhoto || + trimmedSong || + photoFileName, + ); + + const preview = useMemo(() => { + if (!hasAnyInput) return ''; + return buildPreview({ headline, body, sourceUrl, tags }); + }, [body, headline, hasAnyInput, sourceUrl, tags]); + const scheduleHasDraft = scheduledDraft.trim().length > 0; + const editingSchedule = useMemo( + () => savedSchedules.find(schedule => schedule.id === editingScheduleId) || null, + [editingScheduleId, savedSchedules], + ); + + const copyText = async (text, label) => { + const value = text?.trim(); + if (!value) { + toast.warn(`Nothing to copy for ${label}.`); + return; + } + try { + await navigator.clipboard.writeText(value); + toast.success(`${label} copied to clipboard`); + } catch (error) { + toast.error(`Could not copy ${label.toLowerCase()}.`); + } + }; + + const handleReset = () => { + setHeadline(''); + setSourceUrl(''); + setPhotoUrl(''); + setTagsText(''); + setBody(''); + setSong(''); + }; + + const openMyspaceSubmit = () => { + if (typeof window !== 'undefined') { + window.open('https://myspace.com/pages/blog', '_blank', 'noopener,noreferrer'); + } + }; + + const handleScheduleClick = () => { + if (!hasAnyInput) { + toast.error('Nothing to schedule yet. Add details in Make Post first.'); + return; + } + const missingFields = []; + if (!trimmedHeadline) missingFields.push('Blog title / Subject'); + if (tags.length === 0) missingFields.push('Post tags / keywords'); + if (!trimmedBody) missingFields.push('Blog entry'); + if (missingFields.length > 0) { + toast.error(`Add ${missingFields.join(', ')} before scheduling.`); + return; + } + const now = new Date(); + setScheduledDate(formatLocalDate(now)); + setScheduledTime(formatLocalTime(now)); + setScheduledDraft(preview); + setScheduleAttemptedSave(false); + setActiveSubTab('schedule'); + toast.success('Draft moved to Schedule tab.'); + }; + + const removeTag = tagToRemove => { + const remaining = tags.filter(tag => tag !== tagToRemove); + setTagsText(remaining.join(', ')); + }; + + const dropPhotoPreviewOnly = () => { + if (photoPreviewUrl) { + URL.revokeObjectURL(photoPreviewUrl); + setPhotoPreviewUrl(''); + } + if (photoInputRef.current) { + photoInputRef.current.value = ''; + } + }; + + const handlePhotoFileChange = event => { + const file = event.target.files?.[0]; + if (!file) return; + if (photoPreviewUrl) { + URL.revokeObjectURL(photoPreviewUrl); + } + const objectUrl = URL.createObjectURL(file); + setPhotoPreviewUrl(objectUrl); + setPhotoFileName(file.name); + if (photoInputRef.current) { + photoInputRef.current.value = ''; + } + }; + + const handleChoosePhoto = () => { + photoInputRef.current?.click(); + }; + + const handleClearPhoto = () => { + dropPhotoPreviewOnly(); + setPhotoFileName(''); + setPhotoUrl(''); + }; + + const now = new Date(); + const today = formatLocalDate(now); + const currentTime = formatLocalTime(now); + const scheduleTimeMin = scheduledDate === today ? currentTime : '00:00'; + + const handleScheduleDateChange = event => { + const nextDateRaw = event.target.value; + if (!nextDateRaw) return; + const nextDate = nextDateRaw < today ? today : nextDateRaw; + setScheduledDate(nextDate); + setScheduleAttemptedSave(false); + if (nextDate === today) { + const refreshedNow = new Date(); + const refreshedTime = formatLocalTime(refreshedNow); + setScheduledTime(prev => (prev && prev >= refreshedTime ? prev : refreshedTime)); + } + }; + + const handleScheduleTimeChange = event => { + const nextTimeRaw = event.target.value; + if (!nextTimeRaw) return; + if (scheduledDate === today) { + const refreshedNow = new Date(); + const refreshedTime = formatLocalTime(refreshedNow); + setScheduledTime(nextTimeRaw >= refreshedTime ? nextTimeRaw : refreshedTime); + setScheduleAttemptedSave(false); + return; + } + setScheduledTime(nextTimeRaw); + setScheduleAttemptedSave(false); + }; + + const handleBackToMake = () => { + setScheduleAttemptedSave(false); + setActiveSubTab('make'); + }; + + const handleSaveSchedule = () => { + if (scheduleSaving) return; + setScheduleAttemptedSave(true); + if (!scheduleHasDraft) { + toast.warn('Add content to the schedule before saving.'); + return; + } + if (!scheduledDate || !scheduledTime) { + toast.error('Choose a schedule date and time.'); + return; + } + const isEditing = Boolean(editingScheduleId); + const recordId = isEditing ? editingScheduleId : createScheduleId(); + const payload = { + headline, + sourceUrl, + photoUrl, + photoFileName, + tagsText, + tags: [...tags], + body, + song, + scheduledDraft: scheduledDraft.trim(), + scheduledDate, + scheduledTime, + }; + const localRecord = normalizeScheduleRecord({ + ...payload, + id: recordId, + updatedAt: new Date().toISOString(), + }); + setScheduleSaving(true); + let persistSuccess = true; + setSavedSchedules(prev => { + const remaining = prev.filter(item => item.id !== localRecord.id); + const next = sortSchedulesByUpdatedAt([localRecord, ...remaining]); + const { success } = persistSchedulesToStorage(next); + if (!success) { + persistSuccess = false; + } + return next; + }); + if (persistSuccess) { + const toastMessage = isEditing + ? 'Scheduled post updated locally.' + : 'Scheduled post saved locally.'; + toast.success(toastMessage); + setScheduleSyncError(''); + } else { + toast.error('Unable to save schedule in browser storage. It may disappear after refresh.'); + setScheduleSyncError('Unable to sync schedules to browser storage.'); + } + handleReset(); + setScheduledDraft(''); + setScheduledDate(''); + setScheduledTime(''); + setScheduleAttemptedSave(false); + setEditingScheduleId(null); + setActiveSubTab('make'); + setScheduleSaving(false); + }; + + const handleEditSchedule = scheduleId => { + const target = savedSchedules.find(schedule => schedule.id === scheduleId); + if (!target) return; + const refreshedToday = formatLocalDate(new Date()); + let nextDate = target.scheduledDate || refreshedToday; + if (nextDate < refreshedToday) { + nextDate = refreshedToday; + } + let nextTime = target.scheduledTime || '00:00'; + if (nextDate === refreshedToday) { + const refreshedNow = new Date(); + const refreshedTime = formatLocalTime(refreshedNow); + if (!nextTime || nextTime < refreshedTime) { + nextTime = refreshedTime; + } + } + setHeadline(target.headline || ''); + setSourceUrl(target.sourceUrl || ''); + dropPhotoPreviewOnly(); + setPhotoUrl(target.photoUrl || ''); + setPhotoFileName(target.photoFileName || ''); + setTagsText(target.tagsText || ''); + setBody(target.body || ''); + setSong(target.song || ''); + setScheduledDraft(target.scheduledDraft || ''); + setScheduledDate(nextDate); + setScheduledTime(nextTime); + setScheduleAttemptedSave(false); + setEditingScheduleId(target.id); + setActiveSubTab('schedule'); + toast.info('Loaded scheduled post for editing.'); + }; + + return ( +
+
+ {subTabs.map(({ id, label }) => ( + + ))} +
+ + {activeSubTab === 'make' ? ( + <> +
+

Myspace Auto-Poster

+

+ Myspace’s blog composer asks for the same blocks shown here: a subject line, blog + entry, optional photo and song, external link, and keywords. Fill them out once, copy + each one, and then paste directly into the Myspace blog form. +

+
+ +
+
+ +
+
+
+ + + {headline.trim().length}/{HEADLINE_MAX} + +
+ setHeadline(e.target.value)} + className={styles['myspace-field__input']} + placeholder="e.g. Open Source Volunteers Deliver Weekly Progress Platform" + /> + {!trimmedHeadline && ( +

+ Match the “Subject” field from Myspace. Keep it between {HEADLINE_MIN} and{' '} + {HEADLINE_MAX} characters. +

+ )} + {highlightHeadline && ( +

+ Aim for {HEADLINE_MIN}-{HEADLINE_MAX} characters so the title fits the Myspace + subject input. +

+ )} +
+ + +
+
+ +
+
+ +
+ setSourceUrl(e.target.value)} + className={styles['myspace-field__input']} + placeholder="https://" + /> + {!trimmedUrl && ( +

+ Paste the canonical article or project URL if you plan to link to it inside your + Myspace blog entry. +

+ )} + {highlightUrl && ( +

+ Use a fully qualified HTTP(S) link. +

+ )} +
+ +
+
+ +
+
+ + + Mirrors the “Keywords” field on Myspace + +
+