diff --git a/src/components/Announcements/index.jsx b/src/components/Announcements/index.jsx
index d94cc5c6da..38d6a23376 100644
--- a/src/components/Announcements/index.jsx
+++ b/src/components/Announcements/index.jsx
@@ -16,7 +16,9 @@ import {
} from '@fortawesome/free-solid-svg-icons';
import { faFacebook, faLinkedin, faMedium } from '@fortawesome/free-brands-svg-icons';
import ReactTooltip from 'react-tooltip';
-import EmailPanel from './platforms/email';
+
+import EmailPanel from './platforms/email'; // ← new
+import SlashdotAutoPoster from './platforms/slashdot';
function Announcements({ title, email: initialEmail }) {
const [activeTab, setActiveTab] = useState('email');
@@ -169,11 +171,16 @@ function Announcements({ title, email: initialEmail }) {
'livejournal',
'slashdot',
'blogger',
- ].map(platform => (
-
-
-
- ))}
+ 'truthsocial',
+ ].map(platform => {
+ const PlatformComposer =
+ platform === 'slashdot' ? SlashdotAutoPoster : SocialMediaComposer;
+ return (
+
+
+
+ );
+ })}
diff --git a/src/components/Announcements/platforms/slashdot/Helper.jsx b/src/components/Announcements/platforms/slashdot/Helper.jsx
new file mode 100644
index 0000000000..72401a7b0f
--- /dev/null
+++ b/src/components/Announcements/platforms/slashdot/Helper.jsx
@@ -0,0 +1,59 @@
+import React, { useEffect, useState } from 'react';
+import axios from 'axios';
+import { useParams } from 'react-router-dom';
+
+export default function SlashdotHelper() {
+ const { id } = useParams();
+ const [draft, setDraft] = useState(null);
+
+ useEffect(() => {
+ (async () => {
+ const { data } = await axios.get(`/api/slashdot/drafts/${id}`);
+ setDraft(data);
+ })();
+ }, [id]);
+
+ const copy = t => navigator.clipboard.writeText(t || '');
+
+ if (!draft) return
Loading…
;
+
+ return (
+
+
Slashdot Submit Helper
+
+
+ Open{' '}
+
+ slashdot.org/submit
+ {' '}
+ and ensure you’re logged in.
+
+ Copy each field below and paste into the form; then Preview/Submit.
+
+
+
+
+
+
+
+ );
+}
+
+function Field({ title, value, copy }) {
+ return (
+
+
{title}
+
+ {value || '—'}
+
+
copy(value)}>Copy
+
+ );
+}
diff --git a/src/components/Announcements/platforms/slashdot/Slashdot.module.css b/src/components/Announcements/platforms/slashdot/Slashdot.module.css
new file mode 100644
index 0000000000..8046d7e059
--- /dev/null
+++ b/src/components/Announcements/platforms/slashdot/Slashdot.module.css
@@ -0,0 +1,433 @@
+
+
+.slashdot-autoposter {
+ /* max-width: 980px; */
+ width: 100%;
+ margin: 0 auto;
+ display: grid;
+ gap: 24px;
+}
+
+.slashdot-autoposter.dark {
+ color: #dbe6ff;
+}
+
+.slashdot-autoposter.dark label {
+ color: #dbe6ff;
+}
+
+.slashdot-autoposter.dark p {
+ color: #dbe6ff;
+}
+
+.slashdot-subtabs {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 18px;
+ border-bottom: 1px solid #ccd4e0;
+}
+
+.slashdot-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;
+}
+
+.slashdot-subtab:hover {
+ background: #cfcfcf;
+}
+
+.slashdot-subtab.active {
+ background: #d7ecff;
+ color: #0d6efd;
+ border-color: #99c8ff;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6);
+}
+
+.slashdot-autoposter.dark .slashdot-subtab {
+ border-color: transparent;
+ background: #2d3c53;
+ color: #cdd8f6;
+}
+
+.slashdot-autoposter.dark .slashdot-subtab.active {
+ background: #1f4a80;
+ border-color: #1f4a80;
+ color: #fff;
+}
+
+.slashdot-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;
+}
+
+.slashdot-autoposter.dark .slashdot-card {
+ background: #14233a;
+ border-color: #25354d;
+ box-shadow: none;
+}
+
+.slashdot-card.invalid {
+ border-color: #d9534f;
+ box-shadow: 0 0 0 1px rgba(217, 83, 79, 0.18);
+}
+
+.slashdot-autoposter.dark .slashdot-card.invalid {
+ border-color: #ff7b72;
+ box-shadow: none;
+}
+
+.slashdot-grid {
+ display: grid;
+ gap: 20px;
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
+}
+
+.slashdot-field__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ gap: 12px;
+ margin-bottom: 8px;
+}
+
+.slashdot-field__meta {
+ font-size: 0.85rem;
+ color: #6c757d;
+}
+
+.slashdot-field__meta.invalid {
+ color: #d9534f;
+}
+
+.slashdot-autoposter.dark .slashdot-field__meta {
+ color: #9aa9c6;
+}
+.slashdot-field__required {
+ color: #d9534f;
+ margin-left: 4px;
+}
+
+.slashdot-autoposter.dark .slashdot-field__required {
+ color: #ff9384;
+}
+.slashdot-field__input {
+ width: 100%;
+ border: 1px solid #c7d1e5;
+ border-radius: 8px;
+ padding: 12px 14px;
+ font-size: 0.95rem;
+ background: #fff;
+ color: #1b1f29;
+}
+
+.slashdot-autoposter.dark .slashdot-field__input {
+ background: #0f1c2d;
+ border-color: #2b3b55;
+ color: #e4edff;
+}
+
+
+.slashdot-field__input--invalid {
+ border-color: #d9534f;
+ box-shadow: 0 0 0 1px rgba(217, 83, 79, 0.2);
+}
+
+.slashdot-autoposter.dark .slashdot-field__input--invalid {
+ border-color: #ff9384;
+ box-shadow: 0 0 0 1px rgba(255, 147, 132, 0.3);
+}
+.slashdot-field__textarea {
+ resize: vertical;
+ min-height: 110px;
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+
+.slashdot-field__error {
+ color: #d9534f;
+ font-size: 0.85rem;
+ margin-top: 8px;
+}
+
+.slashdot-autoposter.dark .slashdot-field__error {
+ color: #ff9384;
+}
+
+.slashdot-field__hint {
+ color: #6c757d;
+ font-size: 0.85rem;
+ margin-top: 8px;
+}
+
+.slashdot-autoposter.dark .slashdot-field__hint {
+ color: #9aa9c6;
+}
+
+.slashdot-chips {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: 10px;
+}
+
+.slashdot-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;
+}
+
+.slashdot-autoposter.dark .slashdot-chip {
+ background: rgba(13, 110, 253, 0.22);
+ color: #cfe0ff;
+}
+
+.slashdot-chip__label {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.slashdot-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;
+}
+
+.slashdot-chip__clear:hover,
+.slashdot-chip__clear:focus {
+ color: #b7322d;
+ background: transparent;
+}
+
+.slashdot-autoposter.dark .slashdot-chip__clear {
+ color: #ff9384;
+}
+
+.slashdot-preview__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 12px;
+}
+
+.slashdot-preview__actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ justify-content: flex-end;
+}
+
+.slashdot-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;
+}
+
+.slashdot-autoposter.dark .slashdot-preview__body {
+ background: #0f1c2d;
+ border-color: #2b3b55;
+ color: #e4edff;
+}
+
+.slashdot-preview__hint {
+ font-size: 0.85rem;
+ color: #6c757d;
+ margin-top: 12px;
+}
+
+.slashdot-autoposter.dark .slashdot-preview__hint {
+ color: #9aa9c6;
+}
+
+.slashdot-card--scheduler {
+ /* max-width: 720px; */
+ width: 100%;
+}
+
+.slashdot-scheduler__grid {
+ display: grid;
+ gap: 20px;
+ grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
+ align-items: start;
+}
+
+.slashdot-card--saved {
+ max-width: 100%;
+}
+
+.slashdot-scheduler__note {
+ font-size: 0.85rem;
+ color: #6c757d;
+ margin-top: 12px;
+}
+
+.slashdot-autoposter.dark .slashdot-scheduler__note {
+ color: #9aa9c6;
+}
+
+.slashdot-scheduler__controls {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+ margin: 18px 0;
+}
+
+.slashdot-scheduler__field {
+ flex: 1 1 200px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.slashdot-scheduler__textarea {
+ min-height: 220px;
+}
+
+.slashdot-scheduler__actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ margin-top: 18px;
+}
+
+.slashdot-scheduler__empty {
+ font-size: 0.9rem;
+ color: #6c757d;
+ margin: 8px 0 0;
+}
+
+.slashdot-autoposter.dark .slashdot-scheduler__empty {
+ color: #9aa9c6;
+}
+
+.slashdot-saved__list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ margin-top: 18px;
+}
+
+.slashdot-saved__item {
+ border: 1px solid #d6dde7;
+ border-radius: 10px;
+ background: #f4f7fd;
+ padding: 12px 14px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.slashdot-autoposter.dark .slashdot-saved__item {
+ border-color: #2b3b55;
+ background: #0f1c2d;
+}
+
+.slashdot-saved__item--active {
+ border-color: #0d6efd;
+ box-shadow: 0 0 0 1px rgba(13, 110, 253, 0.24);
+}
+
+.slashdot-autoposter.dark .slashdot-saved__item--active {
+ border-color: #4785ff;
+ box-shadow: 0 0 0 1px rgba(71, 133, 255, 0.32);
+}
+
+.slashdot-saved__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ gap: 12px;
+}
+
+.slashdot-saved__title {
+ font-size: 1rem;
+ font-weight: 600;
+ margin: 0;
+ color: #1b1f29;
+}
+
+.slashdot-autoposter.dark .slashdot-saved__title {
+ color: #dbe6ff;
+}
+
+.slashdot-saved__meta {
+ font-size: 0.85rem;
+ color: #6c757d;
+}
+
+.slashdot-autoposter.dark .slashdot-saved__meta {
+ color: #9aa9c6;
+}
+
+.slashdot-saved__excerpt {
+ font-size: 0.9rem;
+ color: #4f5a73;
+ margin: 0;
+}
+
+.slashdot-autoposter.dark .slashdot-saved__excerpt {
+ color: #cfd9f8;
+}
+
+.slashdot-saved__actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+@media (max-width: 960px) {
+ .slashdot-scheduler__grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 640px) {
+ .slashdot-autoposter {
+ gap: 18px;
+ }
+
+ .slashdot-card {
+ padding: 18px;
+ }
+}
diff --git a/src/components/Announcements/platforms/slashdot/index.jsx b/src/components/Announcements/platforms/slashdot/index.jsx
new file mode 100644
index 0000000000..0691c345f6
--- /dev/null
+++ b/src/components/Announcements/platforms/slashdot/index.jsx
@@ -0,0 +1,922 @@
+import React, { useMemo, 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 './Slashdot.module.css';
+
+const HEADLINE_MIN = 12;
+const HEADLINE_MAX = 95;
+const SUMMARY_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 sanitizeTags = text =>
+ text
+ .split(',')
+ .map(tag =>
+ tag
+ .trim()
+ .toLowerCase()
+ .replace(/\s+/g, '-')
+ .replace(/[^a-z0-9-]/g, ''),
+ )
+ .filter(Boolean);
+
+// const slugify = text =>
+// text
+// .toLowerCase()
+// .replace(/[^a-z0-9\s-]/g, '')
+// .replace(/[\s_-]+/g, '-')
+// .replace(/^-+|-+$/g, '')
+// .trim();
+const slugify = text => {
+ const lower = `${text || ''}`.toLowerCase();
+ const chars = [];
+ let lastWasDash = false;
+
+ for (const char of lower) {
+ const code = char.charCodeAt(0);
+ const isAlpha = code >= 97 && code <= 122;
+ const isDigit = code >= 48 && code <= 57;
+
+ if (isAlpha || isDigit) {
+ chars.push(char);
+ lastWasDash = false;
+ continue;
+ }
+
+ if (char === ' ' || char === '-' || char === '_') {
+ if (!lastWasDash && chars.length) {
+ chars.push('-');
+ lastWasDash = true;
+ }
+ }
+ }
+
+ if (chars[chars.length - 1] === '-') {
+ chars.pop();
+ }
+
+ return chars.join('');
+};
+
+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, sourceUrl, dept, tags, intro }) =>
+ `Headline\n${headline?.trim() || '—'}\n\nSource URL\n${sourceUrl?.trim() ||
+ '—'}\n\nDept\n${dept?.trim() || '—'}\n\nTags\n${
+ tags.length ? tags.join(', ') : '—'
+ }\n\nIntro / Summary\n${intro?.trim() || '—'}\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 getSecureBase36 = length => {
+ const chars = [];
+ const max = 36 * 7;
+ while (chars.length < length) {
+ const bytes = new Uint8Array(length);
+ globalThis.crypto.getRandomValues(bytes);
+ for (const byte of bytes) {
+ if (byte >= max) continue;
+ chars.push((byte % 36).toString(36));
+ if (chars.length === length) break;
+ }
+ }
+ return chars.join('');
+};
+const createScheduleId = () => `schedule-${Date.now().toString(36)}-${getSecureBase36(6)}`;
+// const createScheduleId = () =>
+// `schedule-${Date.now().toString(36)}-${Math.random()
+// .toString(36)
+// .slice(2, 8)}`;
+
+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 SlashdotAutoPoster({ platform }) {
+ const darkMode = useSelector(state => state.theme.darkMode);
+
+ const [headline, setHeadline] = useState('');
+ const [sourceUrl, setSourceUrl] = useState('');
+ const [dept, setDept] = useState('');
+ const [tagsText, setTagsText] = useState('');
+ const [intro, setIntro] = 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 [editingScheduleId, setEditingScheduleId] = useState(null);
+ const [scheduleAttemptedSave, setScheduleAttemptedSave] = useState(false);
+
+ 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 trimmedDept = dept.trim();
+ const trimmedIntro = intro.trim();
+
+ const headlineInRange =
+ trimmedHeadline.length >= HEADLINE_MIN && trimmedHeadline.length <= HEADLINE_MAX;
+ const urlValid = /^https?:\/\//i.test(trimmedUrl);
+ const deptValid = trimmedDept.length >= 3;
+ const summaryValid = trimmedIntro.length >= SUMMARY_MIN;
+ const tagsValid = tags.length > 0;
+
+ const readyToCopy = headlineInRange && urlValid && deptValid && summaryValid && tagsValid;
+
+ const highlightHeadline = trimmedHeadline.length > 0 && !headlineInRange;
+ const highlightUrl = trimmedUrl.length > 0 && !urlValid;
+ const highlightDept = trimmedDept.length > 0 && !deptValid;
+ const highlightSummary = trimmedIntro.length > 0 && !summaryValid;
+
+ const hasAnyInput = Boolean(
+ trimmedHeadline || trimmedUrl || trimmedDept || trimmedIntro || tagsText.trim(),
+ );
+
+ const preview = useMemo(() => {
+ if (!hasAnyInput) return '';
+ return buildPreview({ headline, sourceUrl, dept, tags, intro });
+ }, [dept, headline, hasAnyInput, intro, 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('');
+ setDept('');
+ setTagsText('');
+ setIntro('');
+ };
+
+ const openSlashdotSubmit = () => {
+ if (typeof window !== 'undefined') {
+ window.open('https://slashdot.org/submit', '_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('Headline');
+ if (!trimmedUrl) missingFields.push('Source URL');
+ if (!trimmedDept) missingFields.push('Dept');
+ if (tags.length === 0) missingFields.push('Tags');
+ if (!trimmedIntro) missingFields.push('Intro / Summary');
+ 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 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 = () => {
+ 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 record = {
+ id: recordId,
+ headline,
+ sourceUrl,
+ dept,
+ tagsText,
+ tags: [...tags],
+ intro,
+ scheduledDraft: scheduledDraft.trim(),
+ scheduledDate,
+ scheduledTime,
+ updatedAt: new Date().toISOString(),
+ };
+ setSavedSchedules(prev => {
+ const remaining = prev.filter(item => item.id !== record.id);
+ return [record, ...remaining];
+ });
+ const toastMessage = isEditing ? 'Scheduled post updated.' : 'Scheduled post saved.';
+ toast.success(toastMessage);
+ handleReset();
+ setScheduledDraft('');
+ setScheduledDate('');
+ setScheduledTime('');
+ setScheduleAttemptedSave(false);
+ setEditingScheduleId(null);
+ setActiveSubTab('make');
+ };
+
+ 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 || '');
+ setDept(target.dept || '');
+ setTagsText(target.tagsText || '');
+ setIntro(target.intro || '');
+ 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' ? (
+ <>
+
+ Slashdot Auto-Poster
+
+ Slashdot submissions require five pieces: a strong headline, the source URL, a short
+ department slug, relevant tags, and an 80+ character summary. Use the tools below to
+ build a ready-to-submit draft, then copy it (or open slashdot.org/submit) to finish
+ the manual submission.
+
+
+
+ Clear fields
+
+
+
+
+
+
+
+ Headline *
+
+ {headline.trim().length}/{HEADLINE_MAX}
+
+
+
setHeadline(e.target.value)}
+ className={styles['slashdot-field__input']}
+ placeholder="e.g. Open Source Volunteers Deliver Weekly Progress Platform"
+ />
+ {!trimmedHeadline && (
+
+ Add a headline with at least {HEADLINE_MIN} characters and keep it below{' '}
+ {HEADLINE_MAX}.
+
+ )}
+ {highlightHeadline && (
+
+ Aim for {HEADLINE_MIN}-{HEADLINE_MAX} characters so the headline fits Slashdot’s
+ front page.
+
+ )}
+
+ copyText(headline, 'Headline')}
+ >
+ Copy headline
+
+ setHeadline('')}
+ >
+ Clear headline
+
+
+
+
+
+
+ Source URL *
+
+
setSourceUrl(e.target.value)}
+ className={styles['slashdot-field__input']}
+ placeholder="https://"
+ />
+ {!trimmedUrl && (
+
+ Paste the canonical article or project URL.
+
+ )}
+ {highlightUrl && (
+
+ Slashdot only accepts fully qualified HTTP(S) links.
+
+ )}
+
+ copyText(sourceUrl, 'Source URL')}
+ >
+ Copy URL
+
+
+
+
+
+
+ Dept *
+ short slug
+
+
setDept(e.target.value.toLowerCase())}
+ className={styles['slashdot-field__input']}
+ placeholder="e.g. volunteer-tech"
+ />
+ {!trimmedDept && (
+
+ Set the playful department label Slashdot shows under the headline.
+
+ )}
+ {highlightDept && (
+
+ Use a short slug-style phrase with lowercase letters and dashes.
+
+ )}
+
+ setDept(slugify(dept || headline || platform || 'one-community'))}
+ >
+ Suggest dept
+
+ copyText(dept, 'Dept')}
+ >
+ Copy dept
+
+
+
+
+
+
+ Tags *
+ comma separated
+
+
+
+
+
+ Intro / Summary *
+
+ {intro.trim().length} characters
+
+
+
+
+
+
+
+
Submission preview
+
+
+ Schedule this post
+
+
+ Open slashdot.org/submit
+
+ copyText(preview, 'Slashdot draft')}
+ disabled={!readyToCopy}
+ >
+ Copy full draft
+
+
+
+ {preview}
+ {!readyToCopy && (
+
+ Fill every required field to enable copying the complete draft. Slashdot’s form
+ mirrors this layout.
+
+ )}
+
+ >
+ ) : (
+
+
+ Schedule Slashdot 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
+
+
+
+ {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 Slashdot.
+
+
+ {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
+
+
+
+ );
+ })
+ )}
+
+
+
+ )}
+
+ );
+}
+
+SlashdotAutoPoster.propTypes = {
+ platform: PropTypes.string,
+};
+
+SlashdotAutoPoster.defaultProps = {
+ platform: 'slashdot',
+};
+
+export default SlashdotAutoPoster;