phase 4-6 - with buggy contetn calendar page

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-16 16:55:39 +00:00
parent 1f0a31fe79
commit 7e8d667e6e
11 changed files with 3664 additions and 44 deletions

View File

@@ -0,0 +1,219 @@
import React from 'react';
import { Modal } from '../ui/modal';
import Button from '../ui/button/Button';
import { ErrorIcon, CalendarIcon, BoltIcon, ExternalLinkIcon } from '../../icons';
interface Content {
id: number;
title: string;
site_status?: string;
scheduled_publish_at?: string | null;
site_status_updated_at?: string | null;
site_status_message?: string | null;
}
interface Site {
id: number;
name: string;
platform_type: string;
}
interface ErrorDetailsModalProps {
isOpen: boolean;
onClose: () => void;
content: Content | null;
site: Site | null;
onPublishNow: () => void;
onReschedule: () => void;
onFixSettings: () => void;
}
const ErrorDetailsModal: React.FC<ErrorDetailsModalProps> = ({
isOpen,
onClose,
content,
site,
onPublishNow,
onReschedule,
onFixSettings
}) => {
if (!content || !site) return null;
const formatDate = (isoString: string | null) => {
if (!isoString) return 'N/A';
try {
const date = new Date(isoString);
return date.toLocaleString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true
});
} catch (error) {
return isoString;
}
};
const errorMessage = content.site_status_message || 'Publishing failed with no error message.';
// Parse error message to provide helpful suggestions
const getErrorSuggestion = (error: string) => {
const lowerError = error.toLowerCase();
if (lowerError.includes('credential') || lowerError.includes('authentication') || lowerError.includes('403')) {
return 'The publishing site returned an authentication error. Please check the API key in Site Settings.';
} else if (lowerError.includes('timeout') || lowerError.includes('network')) {
return 'The publishing site did not respond in time. Please check your internet connection and site availability.';
} else if (lowerError.includes('404') || lowerError.includes('not found')) {
return 'The publishing endpoint was not found. Please verify the site URL in Site Settings.';
} else if (lowerError.includes('500') || lowerError.includes('server error')) {
return 'The publishing site returned a server error. Please try again later or contact site support.';
} else if (lowerError.includes('required field') || lowerError.includes('missing')) {
return 'Required fields are missing in the content. Please review and complete all necessary fields.';
} else if (lowerError.includes('rate limit')) {
return 'Too many requests were sent to the publishing site. Please wait a few minutes and try again.';
}
return null;
};
const suggestion = getErrorSuggestion(errorMessage);
const platformName = site.platform_type.charAt(0).toUpperCase() + site.platform_type.slice(1);
return (
<Modal
isOpen={isOpen}
onClose={onClose}
showCloseButton={true}
>
<div className="p-6">
{/* Header */}
<div className="flex items-center gap-3 mb-6">
<ErrorIcon className="w-8 h-8 text-error-500" />
<div>
<h2 className="text-xl font-semibold text-gray-900">
Publishing Error Details
</h2>
<p className="text-sm text-gray-500 mt-1">
Content failed to publish
</p>
</div>
</div>
{/* Content Details */}
<div className="space-y-3 mb-6">
<div>
<p className="text-sm font-medium text-gray-700">Content:</p>
<p className="text-sm text-gray-900 mt-1">"{content.title}"</p>
</div>
<div>
<p className="text-sm font-medium text-gray-700">Site:</p>
<p className="text-sm text-gray-900 mt-1">
{site.name} ({platformName})
</p>
</div>
{content.scheduled_publish_at && (
<div>
<p className="text-sm font-medium text-gray-700">Scheduled:</p>
<p className="text-sm text-gray-900 mt-1">
{formatDate(content.scheduled_publish_at)}
</p>
</div>
)}
{content.site_status_updated_at && (
<div>
<p className="text-sm font-medium text-gray-700">Failed:</p>
<p className="text-sm text-gray-900 mt-1">
{formatDate(content.site_status_updated_at)}
</p>
</div>
)}
</div>
{/* Error Message */}
<div className="mb-6">
<p className="text-sm font-medium text-gray-700 mb-2">Error Message:</p>
<div className="bg-error-50 border border-error-200 rounded-lg p-4">
<p className="text-sm text-error-900 whitespace-pre-wrap break-words">
{errorMessage}
</p>
</div>
</div>
{/* Suggestion */}
{suggestion && (
<div className="bg-blue-50 border-l-4 border-blue-500 p-4 mb-6">
<div className="flex items-start gap-2">
<div className="flex-shrink-0">
<svg className="w-5 h-5 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
</div>
<div>
<p className="text-sm font-medium text-blue-900">Suggestion:</p>
<p className="text-sm text-blue-800 mt-1">{suggestion}</p>
</div>
</div>
</div>
)}
{/* Actions */}
<div className="space-y-3">
<p className="text-sm font-medium text-gray-700">Actions:</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<Button
variant="outline"
onClick={() => {
onFixSettings();
onClose();
}}
className="w-full"
>
<ExternalLinkIcon className="w-4 h-4 mr-2" />
Fix Site Settings
</Button>
<Button
variant="outline"
onClick={() => {
onPublishNow();
onClose();
}}
className="w-full"
>
<BoltIcon className="w-4 h-4 mr-2" />
Publish Now
</Button>
<Button
variant="outline"
onClick={() => {
onReschedule();
onClose();
}}
className="w-full"
>
<CalendarIcon className="w-4 h-4 mr-2" />
Reschedule
</Button>
<Button
variant="outline"
onClick={onClose}
className="w-full"
>
Close
</Button>
</div>
</div>
</div>
</Modal>
);
};
export default ErrorDetailsModal;

View File

@@ -298,7 +298,35 @@ const tableActionsConfigs: Record<string, TableActionsConfig> = {
label: 'Publish to Site',
icon: <ArrowRightIcon className="w-5 h-5" />,
variant: 'success',
shouldShow: (row: any) => !row.external_id, // Only show if not published
shouldShow: (row: any) => !row.external_id && row.site_status !== 'scheduled' && row.site_status !== 'publishing', // Only show if not published and not scheduled
},
{
key: 'schedule',
label: 'Schedule',
icon: <BoltIcon className="w-5 h-5" />,
variant: 'primary',
shouldShow: (row: any) => !row.external_id && row.site_status !== 'scheduled' && row.site_status !== 'publishing', // Only show if not published and not scheduled
},
{
key: 'reschedule',
label: 'Reschedule',
icon: <BoltIcon className="w-5 h-5" />,
variant: 'secondary',
shouldShow: (row: any) => row.site_status === 'scheduled', // Only show for scheduled items
},
{
key: 'unschedule',
label: 'Unschedule',
icon: <TrashBinIcon className="w-5 h-5" />,
variant: 'danger',
shouldShow: (row: any) => row.site_status === 'scheduled', // Only show for scheduled items
},
{
key: 'view_error',
label: 'View Error Details',
icon: <CheckCircleIcon className="w-5 h-5 text-danger-500" />,
variant: 'danger',
shouldShow: (row: any) => row.site_status === 'failed', // Only show for failed items
},
{
key: 'view_on_site',
@@ -315,6 +343,18 @@ const tableActionsConfigs: Record<string, TableActionsConfig> = {
icon: <ArrowRightIcon className="w-4 h-4" />,
variant: 'success',
},
{
key: 'bulk_schedule_manual',
label: 'Schedule (Manual)',
icon: <BoltIcon className="w-4 h-4" />,
variant: 'primary',
},
{
key: 'bulk_schedule_defaults',
label: 'Schedule (Site Defaults)',
icon: <BoltIcon className="w-4 h-4" />,
variant: 'primary',
},
{
key: 'export',
label: 'Export Selected',

View File

@@ -21,6 +21,7 @@ import { useToast } from '../../components/ui/toast/ToastContainer';
import { CalendarItemTooltip } from '../../components/ui/tooltip';
import { useSiteStore } from '../../store/siteStore';
import { fetchContent, Content, fetchAPI } from '../../services/api';
import ScheduleContentModal from '../../components/common/ScheduleContentModal';
import {
ClockIcon,
CheckCircleIcon,
@@ -67,10 +68,15 @@ export default function ContentCalendar() {
const [viewMode, setViewMode] = useState<ViewMode>('calendar'); // Default to calendar view
const [draggedItem, setDraggedItem] = useState<Content | null>(null);
const [currentMonth, setCurrentMonth] = useState(new Date()); // Track current month for calendar
// Schedule modal state
const [showScheduleModal, setShowScheduleModal] = useState(false);
const [scheduleContent, setScheduleContent] = useState<Content | null>(null);
const [isRescheduling, setIsRescheduling] = useState(false);
// Derived state: Queue items (scheduled or publishing - exclude already published)
const queueItems = useMemo(() => {
return allContent
const items = allContent
.filter((c: Content) =>
(c.site_status === 'scheduled' || c.site_status === 'publishing') &&
(!c.external_id || c.external_id === '') // Exclude already published items
@@ -80,6 +86,13 @@ export default function ContentCalendar() {
const dateB = b.scheduled_publish_at ? new Date(b.scheduled_publish_at).getTime() : 0;
return dateA - dateB;
});
console.log('[ContentCalendar] queueItems (derived):', items.length, 'items');
items.forEach(item => {
console.log(' Queue item:', item.id, item.title, 'scheduled:', item.scheduled_publish_at);
});
return items;
}, [allContent]);
// Derived state: Published items (have external_id - same logic as Content Approved page)
@@ -98,12 +111,40 @@ export default function ContentCalendar() {
);
}, [allContent]);
// Derived state: Failed items (publish failures)
const failedItems = useMemo(() => {
const items = allContent
.filter((c: Content) => c.site_status === 'failed')
.sort((a: Content, b: Content) => {
// Sort by failure time (most recent first)
const dateA = a.site_status_updated_at ? new Date(a.site_status_updated_at).getTime() : 0;
const dateB = b.site_status_updated_at ? new Date(b.site_status_updated_at).getTime() : 0;
return dateB - dateA;
});
console.log('[ContentCalendar] failedItems (derived):', items.length, 'items');
items.forEach(item => {
console.log(' Failed item:', item.id, item.title, 'error:', item.site_status_message);
});
return items;
}, [allContent]);
// Calculate stats from allContent
const stats = useMemo(() => {
const now = new Date();
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const thirtyDaysFromNow = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
// DEBUG: Check scheduled items in stats calculation
const scheduledItems = allContent.filter((c: Content) =>
c.site_status === 'scheduled' && (!c.external_id || c.external_id === '')
);
console.log('[ContentCalendar] STATS CALCULATION - Scheduled items:', scheduledItems.length);
scheduledItems.forEach(c => {
console.log(' Stats scheduled item:', c.id, c.title, 'external_id:', c.external_id);
});
// Published in last 30 days - check EITHER external_id OR site_status='published'
const publishedLast30Days = allContent.filter((c: Content) => {
const isPublished = (c.external_id && c.external_id !== '') || c.site_status === 'published';
@@ -124,14 +165,13 @@ export default function ContentCalendar() {
return {
// Scheduled count excludes items that are already published
scheduled: allContent.filter((c: Content) =>
c.site_status === 'scheduled' && (!c.external_id || c.external_id === '') && c.site_status !== 'published'
).length,
scheduled: scheduledItems.length,
publishing: allContent.filter((c: Content) => c.site_status === 'publishing').length,
// Published: check EITHER external_id OR site_status='published'
published: allContent.filter((c: Content) =>
(c.external_id && c.external_id !== '') || c.site_status === 'published'
).length,
failed: allContent.filter((c: Content) => c.site_status === 'failed').length,
review: allContent.filter((c: Content) => c.status === 'review').length,
approved: allContent.filter((c: Content) =>
c.status === 'approved' && (!c.external_id || c.external_id === '') && c.site_status !== 'published'
@@ -142,32 +182,99 @@ export default function ContentCalendar() {
}, [allContent]);
const loadQueue = useCallback(async () => {
if (!activeSite?.id) return;
if (!activeSite?.id) {
console.log('[ContentCalendar] No active site selected, skipping load');
return;
}
try {
setLoading(true);
// Fetch all content for this site
const response = await fetchContent({
site_id: activeSite.id,
page_size: 200,
// IMPORTANT: Since content is ordered by -created_at, we need to fetch items by specific site_status
// Otherwise old scheduled/failed items will be on later pages and won't load
console.log('[ContentCalendar] ========== SITE FILTERING DEBUG ==========');
console.log('[ContentCalendar] Active site ID:', activeSite.id);
console.log('[ContentCalendar] Active site name:', activeSite.name);
console.log('[ContentCalendar] Fetching content with multiple targeted queries...');
// Fetch scheduled items (all of them, regardless of page)
const scheduledResponse = await fetchAPI('/v1/writer/content/', {
params: {
site_id: activeSite.id,
page_size: 1000,
site_status: 'scheduled', // Filter specifically for scheduled
}
});
// Debug: Log content with external_id (published status)
console.log('[ContentCalendar] Total content items:', response.results?.length);
console.log('[ContentCalendar] Published items (with external_id):', response.results?.filter(c => c.external_id && c.external_id !== '').length);
console.log('[ContentCalendar] Scheduled items:', response.results?.filter(c => c.site_status === 'scheduled').length);
console.log('[ContentCalendar] Sample content:', response.results?.slice(0, 3).map(c => ({
id: c.id,
title: c.title,
status: c.status,
site_status: c.site_status,
external_id: c.external_id,
scheduled_publish_at: c.scheduled_publish_at,
updated_at: c.updated_at
})));
// Fetch failed items (all of them)
const failedResponse = await fetchAPI('/v1/writer/content/', {
params: {
site_id: activeSite.id,
page_size: 1000,
site_status: 'failed', // Filter specifically for failed
}
});
setAllContent(response.results || []);
// Fetch approved items (for sidebar drag-drop)
const approvedResponse = await fetchAPI('/v1/writer/content/', {
params: {
site_id: activeSite.id,
page_size: 100,
status: 'approved', // Approved workflow status
}
});
// Fetch published items (with external_id) for display
const publishedResponse = await fetchAPI('/v1/writer/content/', {
params: {
site_id: activeSite.id,
page_size: 100,
ordering: '-updated_at', // Most recently published first
}
});
// Combine all results, removing duplicates by ID
const allItems = [
...(scheduledResponse.results || []),
...(failedResponse.results || []),
...(approvedResponse.results || []),
...(publishedResponse.results || []),
];
// Remove duplicates by ID
const uniqueItems = Array.from(
new Map(allItems.map(item => [item.id, item])).values()
);
// Debug: Comprehensive logging
console.log('[ContentCalendar] ========== DATA LOAD DEBUG ==========');
console.log('[ContentCalendar] Scheduled query returned:', scheduledResponse.results?.length, 'items');
console.log('[ContentCalendar] Failed query returned:', failedResponse.results?.length, 'items');
console.log('[ContentCalendar] Approved query returned:', approvedResponse.results?.length, 'items');
console.log('[ContentCalendar] Published query returned:', publishedResponse.results?.length, 'items');
console.log('[ContentCalendar] Total unique items after deduplication:', uniqueItems.length);
console.log('[ContentCalendar] ALL SCHEDULED ITEMS DETAILS:');
scheduledResponse.results?.forEach(c => {
console.log(' - ID:', c.id, '| Title:', c.title);
console.log(' status:', c.status, '| site_status:', c.site_status);
console.log(' scheduled_publish_at:', c.scheduled_publish_at);
console.log(' external_id:', c.external_id);
console.log(' ---');
});
console.log('[ContentCalendar] ALL FAILED ITEMS DETAILS:');
failedResponse.results?.forEach(c => {
console.log(' - ID:', c.id, '| Title:', c.title);
console.log(' status:', c.status, '| site_status:', c.site_status);
console.log(' site_status_message:', c.site_status_message);
console.log(' scheduled_publish_at:', c.scheduled_publish_at);
console.log(' ---');
});
console.log('[ContentCalendar] ====================================');
setAllContent(uniqueItems);
} catch (error: any) {
toast.error(`Failed to load content: ${error.message}`);
} finally {
@@ -175,11 +282,54 @@ export default function ContentCalendar() {
}
}, [activeSite?.id, toast]);
// Load queue when active site changes
useEffect(() => {
if (activeSite?.id) {
console.log('[ContentCalendar] Site changed to:', activeSite.id, activeSite.name);
console.log('[ContentCalendar] Triggering loadQueue...');
loadQueue();
} else {
console.log('[ContentCalendar] No active site, clearing content');
setAllContent([]);
}
}, [activeSite?.id]); // Only depend on activeSite.id, loadQueue is stable
// Reschedule content
const handleRescheduleContent = useCallback(async (contentId: number, scheduledDate: string) => {
try {
await fetchAPI(`/v1/writer/content/${contentId}/reschedule/`, {
method: 'POST',
body: JSON.stringify({ scheduled_at: scheduledDate }),
});
toast.success('Rescheduled successfully');
loadQueue(); // Reload calendar
} catch (error: any) {
toast.error(`Failed to reschedule: ${error.message}`);
throw error;
}
}, [toast, loadQueue]);
// Open reschedule modal
const openRescheduleModal = useCallback((item: Content) => {
setScheduleContent(item);
setIsRescheduling(true);
setShowScheduleModal(true);
}, []);
// Handle schedule/reschedule from modal
const handleScheduleFromModal = useCallback(async (contentId: number, scheduledDate: string) => {
if (isRescheduling) {
await handleRescheduleContent(contentId, scheduledDate);
} else {
const response = await scheduleContent(contentId, scheduledDate);
toast.success('Scheduled successfully');
loadQueue();
}
}, [activeSite?.id]); // Removed loadQueue from dependencies to prevent reload loops
setShowScheduleModal(false);
setScheduleContent(null);
setIsRescheduling(false);
}, [isRescheduling, handleRescheduleContent, toast, loadQueue]);
// Drag and drop handlers for list view
const handleDragStart = (e: React.DragEvent, item: Content, source: 'queue' | 'approved') => {
@@ -589,6 +739,14 @@ export default function ContentCalendar() {
</div>
{getStatusBadge(item)}
<div className="flex items-center gap-1">
<IconButton
icon={<PencilIcon className="w-4 h-4" />}
variant="ghost"
tone="neutral"
size="sm"
onClick={() => openRescheduleModal(item)}
title="Edit schedule"
/>
<IconButton
icon={<EyeIcon className="w-4 h-4" />}
variant="ghost"
@@ -753,8 +911,9 @@ export default function ContentCalendar() {
)}
</div>
{/* Approved Content Sidebar - reduced width by 15% (80 -> 68) */}
<div className="w-68 flex-shrink-0">
{/* Right Sidebar - Contains Approved and Failed Items */}
<div className="w-68 flex-shrink-0 space-y-4">
{/* Approved Content Card */}
<ComponentCard title="Approved Content" desc="Drag to publishing queue to schedule">
<div className="space-y-2 max-h-[600px] overflow-y-auto">
{approvedItems.length === 0 ? (
@@ -797,8 +956,77 @@ export default function ContentCalendar() {
)}
</div>
</ComponentCard>
{/* Failed Items Card - Show below Approved if any exist */}
{failedItems.length > 0 && (
<ComponentCard
title="Failed Publishes"
desc="Items that failed to publish. Review and retry."
headerContent={
<Button
variant="ghost"
size="sm"
onClick={() => navigate('/writer/approved?site_status=failed')}
>
View All
</Button>
}
>
<div className="space-y-2 max-h-[400px] overflow-y-auto">
{failedItems.map(item => (
<div
key={item.id}
className="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800"
>
<h4 className="font-medium text-gray-900 dark:text-white text-sm truncate">
{item.title}
</h4>
<div className="mt-2 space-y-1">
{item.site_status_message && (
<p className="text-xs text-red-600 dark:text-red-400 line-clamp-2">
{item.site_status_message}
</p>
)}
<div className="flex items-center gap-1 pt-1">
<Button
variant="outline"
size="xs"
onClick={() => openRescheduleModal(item)}
startIcon={<CalendarIcon className="w-3 h-3" />}
>
Reschedule
</Button>
<Button
variant="ghost"
size="xs"
onClick={() => navigate(`/writer/content/${item.id}`)}
>
View Details
</Button>
</div>
</div>
</div>
))}
</div>
</ComponentCard>
)}
</div>
</div>
{/* Schedule/Reschedule Modal */}
{showScheduleModal && scheduleContent && (
<ScheduleContentModal
isOpen={showScheduleModal}
onClose={() => {
setShowScheduleModal(false);
setScheduleContent(null);
setIsRescheduling(false);
}}
content={scheduleContent}
onSchedule={handleScheduleFromModal}
mode={isRescheduling ? 'reschedule' : 'schedule'}
/>
)}
</>
);
}

View File

@@ -32,6 +32,7 @@ import PublishLimitModal from '../../components/common/PublishLimitModal';
import ScheduleContentModal from '../../components/common/ScheduleContentModal';
import BulkScheduleModal from '../../components/common/BulkScheduleModal';
import BulkSchedulePreviewModal from '../../components/common/BulkSchedulePreviewModal';
import ErrorDetailsModal from '../../components/common/ErrorDetailsModal';
export default function Approved() {
const toast = useToast();
@@ -91,6 +92,10 @@ export default function Approved() {
const [bulkScheduleItems, setBulkScheduleItems] = useState<Content[]>([]);
const [bulkSchedulePreview, setBulkSchedulePreview] = useState<any>(null);
// Error details modal state
const [showErrorDetailsModal, setShowErrorDetailsModal] = useState(false);
const [errorContent, setErrorContent] = useState<Content | null>(null);
// Load dynamic filter options based on current site's data and applied filters
// This implements cascading filters - each filter's options reflect what's available
// given the other currently applied filters
@@ -482,6 +487,9 @@ export default function Approved() {
if (window.confirm(`Are you sure you want to unschedule "${row.title}"?`)) {
await handleUnscheduleContent(row.id);
}
} else if (action === 'view_error') {
setErrorContent(row);
setShowErrorDetailsModal(true);
} else if (action === 'edit') {
// Navigate to content editor
if (row.site_id) {
@@ -609,8 +617,14 @@ export default function Approved() {
const handleBulkAction = useCallback(async (action: string, ids: string[]) => {
if (action === 'bulk_publish_site') {
await handleBulkPublishToSite(ids);
} else if (action === 'bulk_schedule_manual') {
// Manual bulk scheduling (same time for all)
handleBulkScheduleManual(ids);
} else if (action === 'bulk_schedule_defaults') {
// Schedule with site defaults
handleBulkScheduleWithDefaults(ids);
}
}, [handleBulkPublishToSite]);
}, [handleBulkPublishToSite, handleBulkScheduleManual, handleBulkScheduleWithDefaults]);
// Bulk status update handler
const handleBulkUpdateStatus = useCallback(async (ids: string[], status: string) => {
@@ -886,6 +900,28 @@ export default function Approved() {
onChangeSettings={handleOpenSiteSettings}
siteId={activeSite?.id || 0}
/>
<ErrorDetailsModal
isOpen={showErrorDetailsModal}
onClose={() => {
setShowErrorDetailsModal(false);
setErrorContent(null);
}}
content={errorContent}
site={activeSite}
onPublishNow={() => {
if (errorContent) {
handleSinglePublish(errorContent);
}
}}
onReschedule={() => {
if (errorContent) {
setScheduleContent(errorContent);
setShowScheduleModal(true);
}
}}
onFixSettings={handleOpenSiteSettings}
/>
</>
);
}

View File

@@ -636,8 +636,14 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
const date = new Date(content.scheduled_publish_at);
const localDateTime = date.toISOString().slice(0, 16);
setScheduleDateTime(localDateTime);
} else if (content?.site_status === 'failed') {
// Default to tomorrow at 9 AM for failed items without a schedule
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(9, 0, 0, 0);
setScheduleDateTime(tomorrow.toISOString().slice(0, 16));
}
}, [content?.scheduled_publish_at]);
}, [content?.scheduled_publish_at, content?.site_status]);
// Handler to update schedule
const handleUpdateSchedule = async () => {
@@ -646,11 +652,22 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
setIsUpdatingSchedule(true);
try {
const isoDateTime = new Date(scheduleDateTime).toISOString();
await fetchAPI(`/v1/writer/content/${content.id}/schedule/`, {
// Use reschedule endpoint for failed items, schedule endpoint for scheduled items
const endpoint = content.site_status === 'failed'
? `/v1/writer/content/${content.id}/reschedule/`
: `/v1/writer/content/${content.id}/schedule/`;
const body = content.site_status === 'failed'
? JSON.stringify({ scheduled_at: isoDateTime })
: JSON.stringify({ scheduled_publish_at: isoDateTime });
await fetchAPI(endpoint, {
method: 'POST',
body: JSON.stringify({ scheduled_publish_at: isoDateTime }),
body,
});
toast.success('Schedule updated successfully');
toast.success(content.site_status === 'failed' ? 'Content rescheduled successfully' : 'Schedule updated successfully');
setIsEditingSchedule(false);
// Trigger content refresh by reloading the page
window.location.reload();
@@ -1180,8 +1197,8 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
</span>
</div>
{/* Schedule Date/Time Editor - Only for scheduled content */}
{content.site_status === 'scheduled' && (
{/* Schedule Date/Time Editor - For scheduled and failed content */}
{(content.site_status === 'scheduled' || content.site_status === 'failed') && (
<div className="flex items-center gap-2">
{isEditingSchedule ? (
<>
@@ -1198,7 +1215,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
onClick={handleUpdateSchedule}
disabled={isUpdatingSchedule || !scheduleDateTime}
>
{isUpdatingSchedule ? 'Updating...' : 'Update'}
{isUpdatingSchedule ? 'Updating...' : content.site_status === 'failed' ? 'Reschedule' : 'Update'}
</Button>
<Button
size="xs"
@@ -1206,10 +1223,16 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
tone="neutral"
onClick={() => {
setIsEditingSchedule(false);
// Reset to original value
// Reset to original value or tomorrow if failed
if (content.scheduled_publish_at) {
const date = new Date(content.scheduled_publish_at);
setScheduleDateTime(date.toISOString().slice(0, 16));
} else if (content.site_status === 'failed') {
// Default to tomorrow for failed items
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(9, 0, 0, 0);
setScheduleDateTime(tomorrow.toISOString().slice(0, 16));
}
}}
>
@@ -1219,12 +1242,17 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
) : (
<>
<span className="text-sm text-gray-500 dark:text-gray-400">
{content.scheduled_publish_at ? formatDate(content.scheduled_publish_at) : ''}
{content.site_status === 'failed' && (
content.scheduled_publish_at ? `Was scheduled: ${formatDate(content.scheduled_publish_at)}` : 'Not scheduled'
)}
{content.site_status === 'scheduled' && (
content.scheduled_publish_at ? formatDate(content.scheduled_publish_at) : ''
)}
</span>
<button
onClick={() => setIsEditingSchedule(true)}
className="text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300 p-1"
title="Edit schedule"
title={content.site_status === 'failed' ? 'Reschedule' : 'Edit schedule'}
>
<PencilIcon className="w-4 h-4" />
</button>
@@ -1233,6 +1261,15 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
</div>
)}
{/* Error message for failed items */}
{content.site_status === 'failed' && content.site_status_message && (
<div className="flex items-center gap-2 px-3 py-1 bg-red-50 dark:bg-red-900/20 rounded border border-red-200 dark:border-red-800">
<span className="text-xs text-red-600 dark:text-red-400">
{content.site_status_message}
</span>
</div>
)}
{content.external_url && content.site_status === 'published' && (
<a
href={content.external_url}