From ff44827b35604aa1e4894215cf7cac7490b24479 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Mon, 5 Jan 2026 03:41:17 +0000 Subject: [PATCH] Phase 1 missing file --- frontend/src/pages/Sites/PublishingQueue.tsx | 452 +++++++++++++++++++ 1 file changed, 452 insertions(+) create mode 100644 frontend/src/pages/Sites/PublishingQueue.tsx diff --git a/frontend/src/pages/Sites/PublishingQueue.tsx b/frontend/src/pages/Sites/PublishingQueue.tsx new file mode 100644 index 00000000..332841bd --- /dev/null +++ b/frontend/src/pages/Sites/PublishingQueue.tsx @@ -0,0 +1,452 @@ +/** + * Publishing Queue Page + * Shows scheduled content for publishing to external site + * Allows reordering, pausing, and viewing calendar + */ +import React, { useState, useEffect, useCallback } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import PageMeta from '../../components/common/PageMeta'; +import PageHeader from '../../components/common/PageHeader'; +import ComponentCard from '../../components/common/ComponentCard'; +import { Card } from '../../components/ui/card'; +import Button from '../../components/ui/button/Button'; +import IconButton from '../../components/ui/button/IconButton'; +import { ButtonGroup, ButtonGroupItem } from '../../components/ui/button-group/ButtonGroup'; +import { useToast } from '../../components/ui/toast/ToastContainer'; +import { fetchContent, Content } from '../../services/api'; +import { + ClockIcon, + CheckCircleIcon, + ArrowRightIcon, + CalendarIcon, + ListIcon, + PauseIcon, + PlayIcon, + TrashBinIcon, + EyeIcon, +} from '../../icons'; + +type ViewMode = 'list' | 'calendar'; + +interface QueueItem extends Content { + isPaused?: boolean; +} + +export default function PublishingQueue() { + const { id: siteId } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const toast = useToast(); + + const [loading, setLoading] = useState(true); + const [queueItems, setQueueItems] = useState([]); + const [viewMode, setViewMode] = useState('list'); + const [draggedItem, setDraggedItem] = useState(null); + const [stats, setStats] = useState({ + scheduled: 0, + publishing: 0, + published: 0, + failed: 0, + }); + + const loadQueue = useCallback(async () => { + try { + setLoading(true); + + // Fetch content that is scheduled or publishing + const response = await fetchContent({ + site_id: Number(siteId), + page_size: 100, + }); + + const items = (response.results || []).filter( + (c: Content) => c.site_status === 'scheduled' || c.site_status === 'publishing' + ); + + // Sort by scheduled_publish_at + items.sort((a: Content, b: Content) => { + const dateA = a.scheduled_publish_at ? new Date(a.scheduled_publish_at).getTime() : 0; + const dateB = b.scheduled_publish_at ? new Date(b.scheduled_publish_at).getTime() : 0; + return dateA - dateB; + }); + + setQueueItems(items); + + // Calculate stats + const allContent = response.results || []; + setStats({ + scheduled: allContent.filter((c: Content) => c.site_status === 'scheduled').length, + publishing: allContent.filter((c: Content) => c.site_status === 'publishing').length, + published: allContent.filter((c: Content) => c.site_status === 'published').length, + failed: allContent.filter((c: Content) => c.site_status === 'failed').length, + }); + } catch (error: any) { + toast.error(`Failed to load queue: ${error.message}`); + } finally { + setLoading(false); + } + }, [siteId, toast]); + + useEffect(() => { + if (siteId) { + loadQueue(); + } + }, [siteId, loadQueue]); + + // Drag and drop handlers + const handleDragStart = (e: React.DragEvent, item: QueueItem) => { + setDraggedItem(item); + e.dataTransfer.effectAllowed = 'move'; + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + }; + + const handleDrop = (e: React.DragEvent, targetItem: QueueItem) => { + e.preventDefault(); + if (!draggedItem || draggedItem.id === targetItem.id) return; + + const newItems = [...queueItems]; + const draggedIndex = newItems.findIndex(item => item.id === draggedItem.id); + const targetIndex = newItems.findIndex(item => item.id === targetItem.id); + + // Remove dragged item and insert at target position + newItems.splice(draggedIndex, 1); + newItems.splice(targetIndex, 0, draggedItem); + + setQueueItems(newItems); + setDraggedItem(null); + + // TODO: Call API to update scheduled_publish_at based on new order + toast.success('Queue order updated'); + }; + + const handleDragEnd = () => { + setDraggedItem(null); + }; + + const handlePauseItem = (item: QueueItem) => { + // Toggle pause state (in real implementation, this would call an API) + setQueueItems(prev => + prev.map(i => i.id === item.id ? { ...i, isPaused: !i.isPaused } : i) + ); + toast.info(item.isPaused ? 'Item resumed' : 'Item paused'); + }; + + const handleRemoveFromQueue = (item: QueueItem) => { + // TODO: Call API to set site_status back to 'not_published' + setQueueItems(prev => prev.filter(i => i.id !== item.id)); + toast.success('Removed from queue'); + }; + + const handleViewContent = (item: QueueItem) => { + navigate(`/sites/${siteId}/posts/${item.id}`); + }; + + const formatScheduledTime = (dateStr: string | null | undefined) => { + if (!dateStr) return 'Not scheduled'; + const date = new Date(dateStr); + return date.toLocaleString('en-US', { + weekday: 'short', + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + hour12: true, + }); + }; + + const getStatusBadge = (item: QueueItem) => { + if (item.isPaused) { + return ( + + + Paused + + ); + } + if (item.site_status === 'publishing') { + return ( + + + Publishing... + + ); + } + return ( + + + Scheduled + + ); + }; + + // Calendar view helpers + const getCalendarDays = () => { + const today = new Date(); + const days = []; + for (let i = 0; i < 14; i++) { + const date = new Date(today); + date.setDate(today.getDate() + i); + days.push(date); + } + return days; + }; + + const getItemsForDate = (date: Date) => { + return queueItems.filter(item => { + if (!item.scheduled_publish_at) return false; + const itemDate = new Date(item.scheduled_publish_at); + return ( + itemDate.getDate() === date.getDate() && + itemDate.getMonth() === date.getMonth() && + itemDate.getFullYear() === date.getFullYear() + ); + }); + }; + + if (loading) { + return ( + <> + +
+
Loading queue...
+
+ + ); + } + + return ( + <> + + , color: 'amber' }} + breadcrumb="Sites / Publishing Queue" + /> + + {/* Stats Overview */} +
+ +
+
+ +
+
+

{stats.scheduled}

+

Scheduled

+
+
+
+ +
+
+ +
+
+

{stats.publishing}

+

Publishing

+
+
+
+ +
+
+ +
+
+

{stats.published}

+

Published

+
+
+
+ +
+
+ +
+
+

{stats.failed}

+

Failed

+
+
+
+
+ + {/* View Toggle */} +
+

+ {queueItems.length} items in queue +

+ + setViewMode('list')} + startIcon={} + > + List + + setViewMode('calendar')} + startIcon={} + > + Calendar + + +
+ + {/* Queue Content */} + {queueItems.length === 0 ? ( + + +

+ No content scheduled +

+

+ Content will appear here when it's scheduled for publishing. +

+ +
+ ) : viewMode === 'list' ? ( + /* List View */ + +
+ {queueItems.map((item, index) => ( +
handleDragStart(e, item)} + onDragOver={handleDragOver} + onDrop={(e) => handleDrop(e, item)} + onDragEnd={handleDragEnd} + className={` + flex items-center gap-4 p-4 bg-white dark:bg-gray-800 rounded-lg border-2 + ${draggedItem?.id === item.id ? 'border-brand-500 opacity-50' : 'border-gray-200 dark:border-gray-700'} + ${item.isPaused ? 'opacity-60' : ''} + hover:border-brand-300 dark:hover:border-brand-700 transition-all cursor-move + `} + > + {/* Order number */} +
+ {index + 1} +
+ + {/* Content info */} +
+

+ {item.title} +

+
+ + + {formatScheduledTime(item.scheduled_publish_at)} + + + {item.content_type} +
+
+ + {/* Status badge */} + {getStatusBadge(item)} + + {/* Actions */} +
+ } + variant="ghost" + tone="neutral" + size="sm" + onClick={() => handleViewContent(item)} + title="View content" + /> + : } + variant="ghost" + tone="neutral" + size="sm" + onClick={() => handlePauseItem(item)} + title={item.isPaused ? 'Resume' : 'Pause'} + /> + } + variant="ghost" + tone="danger" + size="sm" + onClick={() => handleRemoveFromQueue(item)} + title="Remove from queue" + /> +
+
+ ))} +
+
+ ) : ( + /* Calendar View */ + +
+ {/* Day headers */} + {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => ( +
+ {day} +
+ ))} + + {/* Calendar days */} + {getCalendarDays().map((date, index) => { + const dayItems = getItemsForDate(date); + const isToday = date.toDateString() === new Date().toDateString(); + + return ( +
+
+ {date.getDate()} +
+
+ {dayItems.slice(0, 3).map(item => ( +
handleViewContent(item)} + className="text-xs p-1 bg-warning-100 dark:bg-warning-900/30 text-warning-800 dark:text-warning-200 rounded truncate cursor-pointer hover:bg-warning-200 dark:hover:bg-warning-900/50" + title={item.title} + > + {item.title} +
+ ))} + {dayItems.length > 3 && ( +
+ +{dayItems.length - 3} more +
+ )} +
+
+ ); + })} +
+
+ )} + + {/* Actions */} +
+ + +
+ + ); +}