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 ? (
) : (
)}
+ {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 }) => (
+ setActiveSubTab(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.
+
+
+
+ Clear fields
+
+
+
+
+
+
+
+ Blog title / Subject *
+
+ {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.
+
+ )}
+
+ copyText(headline, 'Blog title / Subject')}
+ >
+ Copy title
+
+ setHeadline('')}
+ >
+ Clear title
+
+
+
+
+
+
+ External link (optional)
+
+
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.
+
+ )}
+
+ copyText(sourceUrl, 'External link')}
+ >
+ Copy link
+
+
+
+
+
+
+ Post tags / keywords *
+
+ Mirrors the “Keywords” field on Myspace
+
+
+
+
+
+
+ Blog entry *
+
+ {body.trim().length} characters
+
+
+
+
+
+
+
+ Add a photo & song (optional)
+
+ Matches the optional attachments under the Myspace blog form.
+
+
+
+
+
+ Add a photo
+
+
setPhotoUrl(e.target.value)}
+ className={styles['myspace-field__input']}
+ placeholder="https://example.com/myspace-image.jpg"
+ />
+
+
+
+ Choose photo from device
+
+ copyText(photoUrl, 'Photo link')}
+ >
+ Copy photo link
+
+
+ Clear photo
+
+
+ {photoFileName && (
+
+ Selected from device: {photoFileName}
+
+ )}
+ {photoPreviewUrl && (
+
+ )}
+
+
+
+ Add a song
+
+
setSong(e.target.value)}
+ className={styles['myspace-field__input']}
+ placeholder="Artist – Song Title"
+ />
+
+ copyText(song, 'Song')}
+ >
+ Copy song
+
+ setSong('')}
+ >
+ Clear song
+
+
+
+
+
+ Paste hosted links or pick a local photo (saved only for this browser session), plus
+ jot the song info you plan to add inside Myspace.
+
+
+
+
+
+
Submission preview
+
+
+ Schedule this post
+
+
+ Open Myspace blog
+
+ copyText(preview, 'Myspace blog draft')}
+ disabled={!readyToCopy}
+ >
+ Copy full draft
+
+
+
+ {preview}
+ {!readyToCopy && (
+
+ Fill every required field to enable copying the complete draft. The preview lists
+ title, blog entry, external link, and keywords in the exact order Myspace expects.
+
+ )}
+
+ >
+ ) : (
+
+
+ Schedule Myspace Post
+ Scheduling controls, copy below or switch back to Make Post changes.
+ {editingSchedule && (
+
+ Editing saved schedule “{editingSchedule.headline || 'Untitled draft'}”. Saving will
+ overwrite the existing entry.
+
+ )}
+
+
+
+ Scheduled date *
+
+
+ {scheduleAttemptedSave && !scheduledDate && (
+
Select a schedule date.
+ )}
+
+
+
+ Scheduled time *
+
+
+ {scheduleAttemptedSave && !scheduledTime && (
+
Select a schedule time.
+ )}
+
+
+ Scheduled draft
+
+
+
+ {scheduleSaving
+ ? 'Saving…'
+ : editingScheduleId
+ ? 'Update scheduled post'
+ : 'Save scheduled post'}
+
+ copyText(scheduledDraft, 'Scheduled draft')}
+ disabled={!scheduleHasDraft}
+ >
+ Copy scheduled draft
+
+
+ Back to Make Post
+
+
+
+
+
+ Saved scheduled posts
+
+ Choose a saved entry to continue editing or submit it to Myspace. These are stored
+ locally in this browser.
+
+ {schedulesLoading && (
+ Loading saved scheduled posts…
+ )}
+ {scheduleSyncError && (
+ {scheduleSyncError}
+ )}
+
+ {!schedulesLoading && savedSchedules.length === 0 ? (
+
+ No saved scheduled posts yet. Save one to see it listed here.
+
+ ) : (
+ savedSchedules.map(schedule => {
+ const isActive = schedule.id === editingScheduleId;
+ const excerpt =
+ schedule.scheduledDraft && schedule.scheduledDraft.length > 140
+ ? `${schedule.scheduledDraft.slice(0, 140).trim()}...`
+ : schedule.scheduledDraft || 'No summary captured.';
+ return (
+
+
+
+ {schedule.headline || 'Untitled draft'}
+
+
+ {formatDisplayDateTime(schedule.scheduledDate, schedule.scheduledTime)}
+
+
+ {excerpt}
+
+ handleEditSchedule(schedule.id)}
+ >
+ Edit
+
+
+ Submit
+
+
+
+ );
+ })
+ )}
+
+
+
+ )}
+
+ );
+}
+
+MyspaceAutoPoster.propTypes = {
+ platform: PropTypes.string,
+};
+
+MyspaceAutoPoster.defaultProps = {
+ platform: 'myspace',
+};
+
+export default MyspaceAutoPoster;