From f3f92866bc4d74f2379c090f6f0f63c6155e9fa3 Mon Sep 17 00:00:00 2001 From: Sourabh Bagde Date: Sat, 11 Oct 2025 14:58:03 -0500 Subject: [PATCH 01/15] add Slashdot auto-poster - all fields to submit a story. --- src/components/Announcements/index.jsx | 15 +- .../platforms/slashdot/Helper.jsx | 59 ++ .../platforms/slashdot/index.jsx | 559 ++++++++++++++++++ 3 files changed, 628 insertions(+), 5 deletions(-) create mode 100644 src/components/Announcements/platforms/slashdot/Helper.jsx create mode 100644 src/components/Announcements/platforms/slashdot/index.jsx diff --git a/src/components/Announcements/index.jsx b/src/components/Announcements/index.jsx index 93ba38e44b..7c1162bcb1 100644 --- a/src/components/Announcements/index.jsx +++ b/src/components/Announcements/index.jsx @@ -10,6 +10,7 @@ import { faEnvelope } 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'; // ← new +import SlashdotAutoPoster from './platforms/slashdot'; function Announcements({ title, email: initialEmail }) { const [activeTab, setActiveTab] = useState('email'); @@ -129,11 +130,15 @@ function Announcements({ title, email: initialEmail }) { 'slashdot', 'blogger', 'truthsocial', - ].map(platform => ( - - - - ))} + ].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

+
    +
  1. + Open{' '} + + slashdot.org/submit + {' '} + and ensure you’re logged in. +
  2. +
  3. Copy each field below and paste into the form; then Preview/Submit.
  4. +
+ + + + + +
+ ); +} + +function Field({ title, value, copy }) { + return ( +
+

{title}

+
+        {value || '—'}
+      
+ +
+ ); +} diff --git a/src/components/Announcements/platforms/slashdot/index.jsx b/src/components/Announcements/platforms/slashdot/index.jsx new file mode 100644 index 0000000000..1e33eca4f9 --- /dev/null +++ b/src/components/Announcements/platforms/slashdot/index.jsx @@ -0,0 +1,559 @@ +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'; + +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 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 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 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 preview = useMemo(() => buildPreview({ headline, sourceUrl, dept, tags, intro }), [ + headline, + sourceUrl, + dept, + tags, + intro, + ]); + + 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 = () => { + setScheduledDraft(preview); + setActiveSubTab('schedule'); + toast.success('Draft moved to Schedule tab.'); + }; + + const removeTag = tagToRemove => { + const remaining = tags.filter(tag => tag !== tagToRemove); + setTagsText(remaining.join(', ')); + }; + + return ( +
+
+ {subTabs.map(({ 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. +

+
+ +
+
+ +
+
+
+ + + {headline.trim().length}/{HEADLINE_MAX} + +
+ setHeadline(e.target.value)} + className="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. +

+ )} +
+ + +
+
+ +
+
+ +
+ setSourceUrl(e.target.value)} + className="slashdot-field__input" + placeholder="https://" + /> + {!trimmedUrl && ( +

Paste the canonical article or project URL.

+ )} + {highlightUrl && ( +

+ Slashdot only accepts fully qualified HTTP(S) links. +

+ )} +
+ +
+
+ +
+
+ + short slug +
+ setDept(e.target.value.toLowerCase())} + className="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. +

+ )} +
+ + +
+
+ +
+
+ + comma separated +
+