From d47a401f9333b0fa9b0b3e03228ccc4e304b8130 Mon Sep 17 00:00:00 2001 From: Mahitha-pasupuleti Date: Fri, 30 Oct 2026 12:39:57 -0500 Subject: [PATCH 1/4] feat: add Plurk auto-poster component and styles --- src/components/Announcements/index.jsx | 4 + .../Announcements/platforms/index.js | 1 + .../plurk/PlurkAutoPoster.module.css | 425 +++++++++++ .../Announcements/platforms/plurk/index.jsx | 678 ++++++++++++++++++ 4 files changed, 1108 insertions(+) create mode 100644 src/components/Announcements/platforms/plurk/PlurkAutoPoster.module.css create mode 100644 src/components/Announcements/platforms/plurk/index.jsx diff --git a/src/components/Announcements/index.jsx b/src/components/Announcements/index.jsx index 4f9b0c6cc28..f323b32b2c7 100644 --- a/src/components/Announcements/index.jsx +++ b/src/components/Announcements/index.jsx @@ -18,6 +18,7 @@ import styles from './Announcements.module.css'; import EmailPanel from './platforms/email'; import RedditAutoPoster from './platforms/reddit'; import SlashdotAutoPoster from './platforms/slashdot'; +import PlurkAutoPoster from './platforms/plurk'; import SocialMediaComposer from './platforms/social/SocialMediaComposer'; function Announcements({ title, email: initialEmail }) { @@ -246,6 +247,9 @@ function Announcements({ title, email: initialEmail }) { case 'reddit': PlatformComposer = RedditAutoPoster; break; + case 'plurk': + PlatformComposer = PlurkAutoPoster; + break; default: PlatformComposer = SocialMediaComposer; } diff --git a/src/components/Announcements/platforms/index.js b/src/components/Announcements/platforms/index.js index 7c5caec51ad..7a5592fc55d 100644 --- a/src/components/Announcements/platforms/index.js +++ b/src/components/Announcements/platforms/index.js @@ -1,6 +1,7 @@ // Platform exports for Announcements export { default as EmailPanel } from './email'; export { default as SocialMediaComposer } from './social'; +export { default as PlurkAutoPoster } from './plurk'; // Re-export shared components export { default as TinyMCE } from '../shared/tinyMCE'; diff --git a/src/components/Announcements/platforms/plurk/PlurkAutoPoster.module.css b/src/components/Announcements/platforms/plurk/PlurkAutoPoster.module.css new file mode 100644 index 00000000000..a5ba0ac098a --- /dev/null +++ b/src/components/Announcements/platforms/plurk/PlurkAutoPoster.module.css @@ -0,0 +1,425 @@ + +.plurk-autoposter { + width: 100%; + margin: 0 auto; + display: grid; + gap: 24px; + color: #1b1f29; +} + +.plurk-autoposter.dark { + color: #dbe6ff; +} + +.plurk-autoposter label { + display: block; + margin-bottom: 8px; + font-weight: 600; +} + +.plurk-autoposter p { + line-height: 1.6; +} + +.subtabs { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 18px; + border-bottom: 1px solid #ccd4e0; +} + +.subtab { + padding: 9px 16px; + border: 1px solid transparent; + border-bottom: none; + border-radius: 6px 6px 0 0; + background: #d9d9d9; + color: #333; + font-weight: 600; + cursor: pointer; + transition: background 0.2s ease; +} + +.subtab:hover { + background: #cfcfcf; +} + +.subtab.active { + background: #d7ecff; + color: #0d6efd; + border-color: #99c8ff; + box-shadow: inset 0 1px 0 rgb(255 255 255 / 60%); +} + +.dark .subtab { + background: #2d3c53; + color: #cdd8f6; +} + +.dark .subtab.active { + background: #1f4a80; + border-color: #1f4a80; + color: #fff; +} + +.card { + min-width: 0; + padding: 20px 22px; + border: 1px solid #d6dde7; + border-radius: 12px; + background: #fff; + box-shadow: 0 10px 24px rgb(15 37 80 / 8%); + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.dark .card { + background: #14233a; + border-color: #25354d; + box-shadow: none; +} + +.card.invalid { + border-color: #d9534f; + box-shadow: 0 0 0 1px rgb(217 83 79 / 18%); +} + +.grid { + display: grid; + gap: 20px; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); +} + +.topActions, +.fieldActions, +.previewActions, +.schedulerActions, +.savedActions { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 12px; +} + +.fieldHeader { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + margin-bottom: 8px; +} + +.fieldMeta { + color: #6c757d; + font-size: 0.85rem; +} + +.invalidText { + color: #d9534f; +} + +.error { + color: #d9534f; + margin-top: 8px; + font-size: 0.85rem; +} + +.dark .error { + color: #ff9384; +} + +.dark .fieldMeta { + color: #9aa9c6; +} + +.input { + display: block; + width: 100%; + min-width: 0; + padding: 12px 14px; + border: 1px solid #c7d1e5; + border-radius: 8px; + background: #fff; + color: #1b1f29; + font: inherit; + font-size: 0.95rem; +} + +.input:focus { + border-color: #0d6efd; + outline: 2px solid rgb(13 110 253 / 15%); +} + +.dark .input { + background: #0f1c2d; + border-color: #2b3b55; + color: #e4edff; +} + +.textarea { + min-height: 180px; + resize: vertical; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.tagInput { + min-height: 80px; + resize: vertical; +} + +.hint { + margin-top: 8px; + font-size: 0.85rem; +} + +.dark .hint { + color: #9aa9c6; +} + +.chips { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 10px; +} + +.chip { + display: inline-flex; + align-items: center; + gap: 6px; + max-width: 100%; + padding: 4px 10px; + border-radius: 999px; + background: #e9efff; + color: #1c3f82; + font-size: 0.8rem; + font-weight: 600; + overflow-wrap: anywhere; +} + +.dark .chip { + background: rgb(13 110 253 / 22%); + color: #cfe0ff; +} + +.chipClear { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 4px; + border: none; + background: transparent; + color: #d9534f; + font-size: 1.1rem; + cursor: pointer; +} + +.chipClear:hover { + color: #b7322d; +} + +.dark .chipClear { + color: #ff9384; +} + +.previewHeader { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 12px; +} + +.previewHeader h4 { + margin: 0; +} + +.previewActions { + justify-content: flex-end; + margin-top: 0; +} + +.previewBody { + max-height: 320px; + padding: 16px; + overflow: auto; + overflow-wrap: anywhere; + white-space: pre-wrap; + border: 1px solid #d6dde7; + border-radius: 8px; + background: #f8f9fb; + color: #27324b; + font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 0.9rem; +} + +.dark .previewBody { + background: #0f1c2d; + border-color: #2b3b55; + color: #e4edff; +} + +.previewHint { + margin-top: 12px; + color: #6c757d; + font-size: 0.85rem; +} + +.dark .previewHint { + color: #9aa9c6; +} + +.schedulerGrid { + display: grid; + grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); + align-items: start; + gap: 20px; +} + +.schedulerCard, +.savedCard { + width: 100%; + min-width: 0; +} + +.schedulerNote { + margin-top: 12px; + color: #6c757d; + font-size: 0.85rem; +} + +.dark .schedulerNote { + color: #9aa9c6; +} + +.scheduleControls { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin: 18px 0; +} + +.scheduleField { + display: flex; + flex: 1 1 200px; + min-width: 0; + flex-direction: column; + gap: 8px; +} + +.schedulerTextarea { + min-height: 220px; + resize: vertical; +} + +.savedList { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 18px; +} + +.empty { + margin: 8px 0 0; + color: #6c757d; + font-size: 0.9rem; +} + +.dark .empty { + color: #9aa9c6; +} + +.savedItem { + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px 14px; + border: 1px solid #d6dde7; + border-radius: 10px; + background: #f4f7fd; +} + +.dark .savedItem { + background: #0f1c2d; + border-color: #2b3b55; +} + +.savedItemActive { + border-color: #0d6efd; + box-shadow: 0 0 0 1px rgb(13 110 253 / 24%); +} + +.dark .savedItemActive { + border-color: #4785ff; + box-shadow: 0 0 0 1px rgb(71 133 255 / 32%); +} + +.savedHeader { + display: flex; + align-items: baseline; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; +} + +.savedTitle { + margin: 0; + color: #1b1f29; + font-size: 1rem; + font-weight: 600; + overflow-wrap: anywhere; +} + +.dark .savedTitle { + color: #dbe6ff; +} + +.savedMeta { + color: #6c757d; + font-size: 0.85rem; +} + +.dark .savedMeta { + color: #9aa9c6; +} + +.savedExcerpt { + margin: 0; + color: #4f5a73; + font-size: 0.9rem; + overflow-wrap: anywhere; +} + +.dark .savedExcerpt { + color: #cfd9f8; +} + +.savedActions { + margin-top: 0; +} + +@media (width <= 960px) { + .schedulerGrid { + grid-template-columns: minmax(0, 1fr); + } +} + +@media (width <= 640px) { + .plurk-autoposter { + gap: 18px; + } + + .card { + padding: 18px; + } + + .previewHeader { + align-items: flex-start; + } + + .previewActions { + justify-content: flex-start; + } +} \ No newline at end of file diff --git a/src/components/Announcements/platforms/plurk/index.jsx b/src/components/Announcements/platforms/plurk/index.jsx new file mode 100644 index 00000000000..72592c30b1a --- /dev/null +++ b/src/components/Announcements/platforms/plurk/index.jsx @@ -0,0 +1,678 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import classNames from 'classnames'; +import { useSelector } from 'react-redux'; +import { toast } from 'react-toastify'; +import styles from './PlurkAutoPoster.module.css'; + +const PLURK_MAX = 360; +const STORAGE_KEY = 'plurk-auto-poster-schedules'; + +const pad = value => String(value).padStart(2, '0'); + +const formatDate = date => + `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; + +const formatTime = date => `${pad(date.getHours())}:${pad(date.getMinutes())}`; + +const formatDateTime = (date, time) => { + if (!date) return 'Date unavailable'; + + const parsed = new Date(`${date}T${time || '00:00'}`); + + return Number.isNaN(parsed.getTime()) + ? date + : parsed.toLocaleString(undefined, { + dateStyle: 'medium', + timeStyle: time ? 'short' : undefined, + }); +}; + +const makeId = () => + `plurk-${Date.now()}-${Math.random() + .toString(36) + .slice(2, 8)}`; + +const readSchedules = () => { + try { + const stored = localStorage.getItem(STORAGE_KEY); + return stored ? JSON.parse(stored) : []; + } catch { + return []; + } +}; + +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: '#6554d9', color: '#fff' }; + } + + if (variant === 'outline') { + return { + ...base, + backgroundColor: 'transparent', + color: darkMode ? '#9bb5ff' : '#6554d9', + border: `1px solid ${darkMode ? '#3d4d6d' : '#6554d9'}`, + }; + } + + return { + ...base, + backgroundColor: darkMode ? '#1c2b44' : '#eeeaff', + color: darkMode ? '#cfd9f8' : '#5141a8', + }; +}; + +function PlurkAutoPoster() { + const darkMode = useSelector(state => state.theme?.darkMode); + + const [activeTab, setActiveTab] = useState('make'); + const [content, setContent] = useState(''); + const [qualifier, setQualifier] = useState('says'); + const [scheduledDraft, setScheduledDraft] = useState(''); + const [scheduledQualifier, setScheduledQualifier] = useState('says'); + const [scheduledDate, setScheduledDate] = useState(() => formatDate(new Date())); + const [scheduledTime, setScheduledTime] = useState(() => formatTime(new Date())); + const [savedSchedules, setSavedSchedules] = useState(readSchedules); + const [editingId, setEditingId] = useState(null); + const [scheduleAttemptedSave, setScheduleAttemptedSave] = useState(false); + + const trimmedContent = content.trim(); + const contentValid = trimmedContent.length > 0 && trimmedContent.length <= PLURK_MAX; + const hasInput = Boolean(trimmedContent); + const readyToCopy = contentValid; + + const preview = useMemo(() => { + if (!hasInput) return ''; + return `${qualifier} ${trimmedContent}`; + }, [qualifier, trimmedContent, hasInput]); + + const editingSchedule = useMemo( + () => savedSchedules.find(item => item.id === editingId) || null, + [savedSchedules, editingId], + ); + + const scheduleHasDraft = Boolean(scheduledDraft.trim()); + + useEffect(() => { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(savedSchedules)); + } catch { + toast.error('Could not save scheduled posts in this browser.'); + } + }, [savedSchedules]); + + const copyText = async (text, label) => { + if (!text?.trim()) { + toast.warn(`Nothing to copy for ${label}.`); + return; + } + + try { + await navigator.clipboard.writeText(text); + toast.success(`${label} copied to clipboard.`); + } catch { + toast.error(`Could not copy ${label.toLowerCase()}.`); + } + }; + + const resetEditor = () => { + setContent(''); + setQualifier('says'); + }; + + const validatePost = () => { + if (!trimmedContent) { + toast.error('Please enter your Plurk post content.'); + return false; + } + + if (trimmedContent.length > PLURK_MAX) { + toast.error(`Plurk content must not exceed ${PLURK_MAX} characters.`); + return false; + } + + return true; + }; + + const handleReset = () => { + resetEditor(); + toast.info('Post editor cleared.'); + }; + + const handleScheduleClick = () => { + if (!validatePost()) return; + + const now = new Date(); + setScheduledDate(formatDate(now)); + setScheduledTime(formatTime(now)); + setScheduledDraft(trimmedContent); + setScheduledQualifier(qualifier); + setScheduleAttemptedSave(false); + setEditingId(null); + setActiveTab('schedule'); + toast.success('Post moved to the Schedule tab.'); + }; + + const handleScheduleDateChange = event => { + const nextDate = event.target.value; + if (!nextDate) return; + + const today = formatDate(new Date()); + const safeDate = nextDate < today ? today : nextDate; + + setScheduledDate(safeDate); + setScheduleAttemptedSave(false); + + if (safeDate === today) { + const currentTime = formatTime(new Date()); + setScheduledTime(previous => (!previous || previous < currentTime ? currentTime : previous)); + } + }; + + const handleScheduleTimeChange = event => { + const nextTime = event.target.value; + if (!nextTime) return; + + if (scheduledDate === formatDate(new Date())) { + const currentTime = formatTime(new Date()); + setScheduledTime(nextTime < currentTime ? currentTime : nextTime); + } else { + setScheduledTime(nextTime); + } + + setScheduleAttemptedSave(false); + }; + + const handleSaveSchedule = () => { + setScheduleAttemptedSave(true); + + if (!scheduleHasDraft) { + toast.warn('Add a Plurk post to the schedule first.'); + return; + } + + if (scheduledDraft.trim().length > PLURK_MAX) { + toast.error(`Plurk content must not exceed ${PLURK_MAX} characters.`); + return; + } + + if (!scheduledDate || !scheduledTime) { + toast.error('Choose a schedule date and time.'); + return; + } + + const scheduledAt = new Date(`${scheduledDate}T${scheduledTime}`); + + if (Number.isNaN(scheduledAt.getTime()) || scheduledAt <= new Date()) { + toast.error('Choose a future date and time.'); + return; + } + + const id = editingId || makeId(); + const record = { + id, + content: scheduledDraft.trim(), + qualifier: scheduledQualifier, + scheduledDate, + scheduledTime, + updatedAt: new Date().toISOString(), + status: 'scheduled', + }; + + setSavedSchedules(previous => [record, ...previous.filter(item => item.id !== id)]); + + toast.success( + editingId ? 'Scheduled Plurk post updated.' : 'Plurk post saved to your schedule.', + ); + + resetEditor(); + setScheduledDraft(''); + setScheduledQualifier('says'); + setScheduledDate(formatDate(new Date())); + setScheduledTime(formatTime(new Date())); + setScheduleAttemptedSave(false); + setEditingId(null); + setActiveTab('make'); + }; + + const handleEditSchedule = item => { + const today = formatDate(new Date()); + let date = item.scheduledDate || today; + let time = item.scheduledTime || formatTime(new Date()); + + if (date < today) date = today; + + if (date === today && time < formatTime(new Date())) { + time = formatTime(new Date()); + } + + setContent(item.content || ''); + setQualifier(item.qualifier || 'says'); + setScheduledDraft(item.content || ''); + setScheduledQualifier(item.qualifier || 'says'); + setScheduledDate(date); + setScheduledTime(time); + setEditingId(item.id); + setScheduleAttemptedSave(false); + setActiveTab('schedule'); + toast.info('Loaded scheduled Plurk post for editing.'); + }; + + const handleDeleteSchedule = id => { + setSavedSchedules(previous => previous.filter(item => item.id !== id)); + + if (editingId === id) { + setEditingId(null); + setScheduledDraft(''); + } + + toast.info('Scheduled Plurk post deleted.'); + }; + + const removeScheduledDraft = () => { + setScheduledDraft(''); + setEditingId(null); + setScheduleAttemptedSave(false); + }; + + const openPlurk = () => { + window.open('https://www.plurk.com/', '_blank', 'noopener,noreferrer'); + }; + + const today = formatDate(new Date()); + const scheduleTimeMin = scheduledDate === today ? formatTime(new Date()) : undefined; + + return ( +
+
+ {[ + { id: 'make', label: '📝 Make Plurk' }, + { id: 'schedule', label: '⏰ Scheduled Posts' }, + ].map(tab => ( + + ))} +
+ + {activeTab === 'make' ? ( + <> +
+

Plurk Auto-Poster

+

+ Compose a short Plurk post, choose a qualifier, preview the result, and save it for a + future date and time. This frontend stores scheduled posts locally and does not + publish to Plurk. +

+ +
+ +
+
+ +
+
+
+ + + {content.length}/{PLURK_MAX} + +
+ +