/** * Content Plan Item Sidebar Component * * A sidebar component for creating and editing content plan items */ import React, { useState, useEffect, useRef } from 'react'; import type { FC, FormEvent, ChangeEvent } from 'react'; import { __ } from '@wordpress/i18n'; import Select from 'react-select'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; import { useContentPlan } from '../context/ContentPlanContext'; import DynamicListField from './DynamicListField'; import KeywordInput from './KeywordInput'; import DeleteConfirmationModal from './DeleteConfirmationModal'; import ErrorMessage from './ErrorMessage'; import { ContentPlanItem, ContentPlanItemFormData, ValidationErrors, } from '../types'; import { LANGUAGE_OPTIONS } from '../utils/languageOptions'; /** * Safely parse a date string for DatePicker. Returns null for invalid dates * to prevent "Invalid time value" errors from date-fns format(). */ const parseDateSafe = (value: string | null | undefined): Date | null => { if (!value || typeof value !== 'string') return null; const trimmed = value.trim(); if (!trimmed) return null; const date = new Date(trimmed); return isNaN(date.getTime()) ? null : date; }; /** * Extract YYYY-MM-DD from scheduled_on (handles "YYYY-MM-DD H:i:s" and ISO formats). */ const getDatePart = (scheduledOn: string | null | undefined): string => { if (!scheduledOn || typeof scheduledOn !== 'string') return ''; const part = scheduledOn.split(' ')[0] || scheduledOn.split('T')[0] || ''; return part.substring(0, 10) || ''; }; const ContentPlanItemSidebar: FC = () => { // Custom styles for react-select const selectStyles = { control: (provided: any, state: any) => ({ ...provided, minHeight: '48px', border: state.isFocused ? '1px solid #3b82f6' : '1px solid #d1d5db', borderRadius: '6px', boxShadow: state.isFocused ? '0 0 0 1px #3b82f6' : 'none', '&:hover': { border: '1px solid #3b82f6', }, }), valueContainer: (provided: any) => ({ ...provided, padding: '0 12px', }), input: (provided: any) => ({ ...provided, margin: '0', }), indicatorSeparator: () => ({ display: 'none', }), indicatorsContainer: (provided: any) => ({ ...provided, paddingRight: '12px', }), dropdownIndicator: (provided: any) => ({ ...provided, color: '#6b7280', '&:hover': { color: '#374151', }, }), option: (provided: any, state: any) => ({ ...provided, backgroundColor: state.isSelected ? '#3b82f6' : state.isFocused ? '#f3f4f6' : 'white', color: state.isSelected ? 'white' : '#374151', padding: '12px 16px', '&:hover': { backgroundColor: state.isSelected ? '#3b82f6' : '#f3f4f6', }, }), menu: (provided: any) => ({ ...provided, borderRadius: '6px', border: '1px solid #e5e7eb', boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', }), }; const { sidebarOpen, sidebarType, closeSidebar, getString, error, clearError, setErrorState, createContentPlanItem, updateContentPlanItemAPI, fetchPosts, posts, } = useContentPlan(); const [formData, setFormData] = useState({ title: '', structure: [], keywords: [], status: 'planned', scheduled_on: '', post: '', language: 'English', }); const [editingItem, setEditingItem] = useState( undefined ); const [saving, setSaving] = useState(false); const [currentPlanId] = useState(1); // Default to plan ID 1 for now const [validationErrors, setValidationErrors] = useState( {} ); const [deleteModalOpen, setDeleteModalOpen] = useState(false); // Reset form when sidebar opens/closes useEffect(() => { if (sidebarOpen && sidebarType === 'content-plan-item') { // Form will be populated by the parent component } else if (!sidebarOpen) { resetForm(); } }, [sidebarOpen, sidebarType]); // Fetch posts when sidebar opens useEffect(() => { if (sidebarOpen && sidebarType === 'content-plan-item') { fetchPosts(['id', 'title', 'slug', 'status'], ['post', 'page'], { limit: 50, orderby: 'date', order: 'DESC', }); } }, [sidebarOpen, sidebarType]); // Prevent body scroll when sidebar is open useEffect(() => { if (sidebarOpen && sidebarType === 'content-plan-item') { // Save current overflow value const originalOverflow = document.body.style.overflow; // Prevent scrolling document.body.style.overflow = 'hidden'; // Cleanup: restore original overflow when sidebar closes return () => { document.body.style.overflow = originalOverflow; }; } }, [sidebarOpen, sidebarType]); const handleInputChange = ( field: keyof ContentPlanItemFormData, value: any ): void => { setFormData(prev => ({ ...prev, [field]: value, })); // Clear validation error for this field when user starts typing if (validationErrors[field]) { setValidationErrors(prev => { const newErrors = { ...prev }; delete newErrors[field]; return newErrors; }); } }; const validateForm = (): boolean => { const errors: ValidationErrors = {}; // Title is required if (!formData.title.trim()) { errors.title = getString( 'content_plan_table', 'title_required', 'Title is required' ); } // Structure is required if (!formData.structure.length) { errors.structure = getString( 'content_plan_table', 'structure_required', 'At least one structure item is required' ); } // Scheduled On is required if (!formData.scheduled_on) { errors.scheduled_on = getString( 'content_plan_table', 'scheduled_on_required', 'Scheduled date is required' ); } // Keywords are optional, so no validation needed setValidationErrors(errors); return Object.keys(errors).length === 0; }; const handleSubmit = async ( e: FormEvent ): Promise => { e.preventDefault(); // Validate all required fields if (!validateForm()) { return; } setSaving(true); setErrorState(null); try { const itemData = { title: formData.title.trim(), structure: JSON.stringify(formData.structure), keywords: formData.keywords.join(', '), status: formData.status, scheduled_on: formData.scheduled_on ? `${formData.scheduled_on}T00:00:00` : null, post: formData.post.trim() || null, lang: formData.language || null, }; if (editingItem) { await updateContentPlanItemAPI( editingItem.id, itemData, currentPlanId ); } else { await createContentPlanItem(itemData, currentPlanId); } closeSidebar(); resetForm(); } catch (err: any) { setErrorState(err.message); console.error('Error saving content plan item:', err); } finally { setSaving(false); } }; const handleClose = (): void => { closeSidebar(); resetForm(); }; const resetForm = (): void => { setFormData({ title: '', structure: [], keywords: [], status: 'planned', scheduled_on: '', post: '', language: 'English', }); setEditingItem(undefined); setErrorState(null); setValidationErrors({}); }; // Listen for edit events from parent component useEffect(() => { const handleEditItem = ( event: CustomEvent<{ item: ContentPlanItem }> ) => { const { item } = event.detail; setEditingItem(item); let structureArray = []; try { structureArray = item.structure ? JSON.parse(item.structure) : []; if (!Array.isArray(structureArray)) { structureArray = []; } } catch (e) { console.error('Error parsing structure:', e); } setFormData({ title: item.title || '', structure: structureArray, keywords: item.keywords ? item.keywords.split(',').map(k => k.trim()) : [], status: item.status || 'planned', scheduled_on: getDatePart(item.scheduled_on), post: item.post || '', language: item.lang || 'English', }); }; window.addEventListener( 'editContentPlanItem', handleEditItem as EventListener ); return () => { window.removeEventListener( 'editContentPlanItem', handleEditItem as EventListener ); }; }, []); if (!sidebarOpen || sidebarType !== 'content-plan-item') { return null; } return ( <> {/* Backdrop */}
{/* Sidebar */}

{editingItem ? getString( 'content_plan_table', 'edit_item', 'Edit Content Plan Item' ) : getString( 'content_plan_table', 'add_item', 'Add New Content Plan Item' )}

{/* Error Message */} {error && (
)} {/* Title */}
) => handleInputChange('title', e.target.value) } className={`cpepai-form-input h-10 sm:h-12 px-2 sm:px-3 py-2 sm:py-3 text-sm sm:text-base ${ validationErrors.title ? 'cpepai-form-input--error' : '' }`} placeholder={getString( 'content_plan_table', 'title_placeholder', 'Enter item title...' )} required /> {validationErrors.title && (
{validationErrors.title}
)}
{/* Structure */}
handleInputChange('structure', items) } getString={getString} /> {validationErrors.structure && (
{validationErrors.structure}
)}
{/* Status */}
p.id.toString() === formData.post )?.title || formData.post, } : null } onChange={selectedOption => handleInputChange( 'post', selectedOption?.value || '' ) } options={[ { value: '', label: getString( 'content_plan_table', 'select_post', 'Select a post...' ), }, ...(posts.length > 0 ? posts.map(post => ({ value: post.id.toString(), label: `${post.title} (${post.status})`, })) : [ { value: '', label: getString( 'content_plan_table', 'no_posts_available', 'No posts available' ), }, ]), ]} styles={selectStyles} isSearchable={false} className="react-select-container" classNamePrefix="react-select" />
)} {/* Keywords */}
handleInputChange('keywords', keywords) } getString={getString} />
{/* Language */}