Phase 1-3 Implemented - PUBLISHING-PROGRESS-AND-SCHEDULING-UX-PLAN
This commit is contained in:
185
frontend/src/components/common/ScheduleContentModal.tsx
Normal file
185
frontend/src/components/common/ScheduleContentModal.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal } from '../ui/modal';
|
||||
import Button from '../ui/button/Button';
|
||||
import { CalendarIcon, ClockIcon } from '../../icons';
|
||||
|
||||
interface Content {
|
||||
id: number;
|
||||
title: string;
|
||||
scheduled_publish_at?: string | null;
|
||||
}
|
||||
|
||||
interface ScheduleContentModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
content: Content | null;
|
||||
onSchedule: (contentId: number, scheduledDate: string) => Promise<void>;
|
||||
mode?: 'schedule' | 'reschedule';
|
||||
}
|
||||
|
||||
const ScheduleContentModal: React.FC<ScheduleContentModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
content,
|
||||
onSchedule,
|
||||
mode = 'schedule'
|
||||
}) => {
|
||||
const [selectedDate, setSelectedDate] = useState<string>('');
|
||||
const [selectedTime, setSelectedTime] = useState<string>('09:00');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && content) {
|
||||
if (mode === 'reschedule' && content.scheduled_publish_at) {
|
||||
// Pre-fill with existing schedule
|
||||
const existingDate = new Date(content.scheduled_publish_at);
|
||||
const dateStr = existingDate.toISOString().split('T')[0];
|
||||
const hours = existingDate.getHours().toString().padStart(2, '0');
|
||||
const minutes = existingDate.getMinutes().toString().padStart(2, '0');
|
||||
setSelectedDate(dateStr);
|
||||
setSelectedTime(`${hours}:${minutes}`);
|
||||
} else {
|
||||
// Default to tomorrow at 9:00 AM
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const dateStr = tomorrow.toISOString().split('T')[0];
|
||||
setSelectedDate(dateStr);
|
||||
setSelectedTime('09:00');
|
||||
}
|
||||
}
|
||||
}, [isOpen, content, mode]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!content || !selectedDate || !selectedTime) return;
|
||||
|
||||
// Combine date and time into ISO string
|
||||
const scheduledDateTime = new Date(`${selectedDate}T${selectedTime}`);
|
||||
|
||||
// Validate future date
|
||||
if (scheduledDateTime <= new Date()) {
|
||||
alert('Please select a future date and time');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await onSchedule(content.id, scheduledDateTime.toISOString());
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Failed to schedule:', error);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatPreviewDate = () => {
|
||||
if (!selectedDate || !selectedTime) return '';
|
||||
|
||||
try {
|
||||
const dateTime = new Date(`${selectedDate}T${selectedTime}`);
|
||||
return dateTime.toLocaleString('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
if (!content) return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
showCloseButton={!isSubmitting}
|
||||
>
|
||||
<div className="p-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<CalendarIcon className="w-8 h-8 text-primary-500" />
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
{mode === 'reschedule' ? 'Reschedule' : 'Schedule'} Content Publishing
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Content: "{content.title}"
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date/Time Selection */}
|
||||
<div className="space-y-4">
|
||||
{/* Date Picker */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Schedule Date
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
onChange={(e) => setSelectedDate(e.target.value)}
|
||||
min={new Date().toISOString().split('T')[0]}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<CalendarIcon className="absolute right-3 top-2.5 w-5 h-5 text-gray-400 pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Time Picker */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Schedule Time
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="time"
|
||||
value={selectedTime}
|
||||
onChange={(e) => setSelectedTime(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<ClockIcon className="absolute right-3 top-2.5 w-5 h-5 text-gray-400 pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
{selectedDate && selectedTime && (
|
||||
<div className="bg-blue-50 border-l-4 border-blue-500 p-4 rounded">
|
||||
<p className="text-sm font-medium text-blue-900">
|
||||
Preview: {formatPreviewDate()}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3 mt-6 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting || !selectedDate || !selectedTime}
|
||||
>
|
||||
{isSubmitting ? 'Scheduling...' : (mode === 'reschedule' ? 'Reschedule' : 'Schedule')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScheduleContentModal;
|
||||
Reference in New Issue
Block a user