ContentScheduler.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. import * as React from 'react';
  2. import { AIGCVideo, ScheduledPost, SocialAccount } from '../types';
  3. import { Icon } from './ui/Icon';
  4. import { format, startOfMonth, endOfMonth, startOfWeek, endOfWeek, eachDayOfInterval, addMonths, subMonths, addWeeks, subWeeks, addDays, subDays, isSameMonth, isToday, parseISO, isPast, getDay, isSameDay } from 'date-fns';
  5. import { useTranslation } from '../hooks/useI18n';
  6. const initialSocialAccounts: SocialAccount[] = [
  7. {
  8. id: 'tw',
  9. platform: 'Twitter',
  10. username: 'greenpage_ai',
  11. icon: <svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 fill-current text-[#1DA1F2]">
  12. <title>Twitter</title>
  13. <path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.223.085a4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z" />
  14. </svg>
  15. },
  16. {
  17. id: 'fb',
  18. platform: 'Facebook',
  19. username: 'GreenPageApp',
  20. icon: <svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 fill-current text-[#1877F2]">
  21. <title>Facebook</title>
  22. <path d="M22.675 0H1.325C.593 0 0 .593 0 1.325v21.351C0 23.407.593 24 1.325 24H12.82v-9.294H9.692v-3.622h3.128V8.413c0-3.1 1.893-4.788 4.659-4.788 1.325 0 2.463.099 2.795.143v3.24l-1.918.001c-1.504 0-1.795.715-1.795 1.763v2.313h3.587l-.467 3.622h-3.12V24h6.116c.732 0 1.325-.593 1.325-1.325V1.325C24 .593 23.407 0 22.675 0z" />
  23. </svg>
  24. },
  25. {
  26. id: 'ig',
  27. platform: 'Instagram',
  28. username: 'greenpage.ai',
  29. icon: <svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 fill-current">
  30. <title>Instagram</title>
  31. <defs>
  32. <radialGradient id="ig-grad" gradientUnits="userSpaceOnUse" r="150%" cx="30%" cy="107%">
  33. <stop stopColor="#fdf497" offset="0" />
  34. <stop stopColor="#fdf497" offset="0.05" />
  35. <stop stopColor="#fd5949" offset="0.45" />
  36. <stop stopColor="#d6249f" offset="0.6" />
  37. <stop stopColor="#285AEB" offset="0.9" />
  38. </radialGradient>
  39. </defs>
  40. <path fill="url(#ig-grad)" d="M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.784.3-.986.623-1.77.933-.448.174-.9.34-1.356.59-.446.243-.7.4-.933.71-.24.3-.466.737-.62 1.14-.156.4-.3 1.07-.343 1.76-.05.8-.07 1.37-.07 4.23s.02 3.43.07 4.23c.044.68.2 1.36.343 1.76.155.4.38.84.62 1.14.233.3.486.467.933.71.457.25.908.417 1.356.59.783.31 1.2.63 1.77.933.765.3 1.635.5 2.913.56.05.002.1.003.15.003.25 0 .5 0 .75-.004 1.28-.056 2.15-.26 2.913-.56.784-.3 1.2-.623 1.77-.933.448-.174-.9.34 1.356.59.446-.243-.7-.4.933-.71.24-.3.466.737-.62-1.14.156-.4.3-1.07.343-1.76.05-.8.07-1.37.07-4.23s-.02-3.43-.07-4.23c-.044-.68-.2-1.36-.343-1.76-.155-.4-.38-.84-.62-1.14-.233-.3-.486-.467-.933-.71-.457-.25-.908.417-1.356.59-.783-.31-1.2-.63-1.77-.933-.765-.3-1.635-.5-2.913-.56-.25-.003-.5-.004-.75-.004zm0 2.16c3.203 0 3.585.016 4.85.07 1.17.055 1.805.248 2.227.415.562.217.96.477 1.382.896.413.42.67.824.896 1.383.167.422.36 1.057.413 2.227.055 1.265.07 1.646.07 4.85s-.015 3.585-.07 4.85c-.055 1.17-.248 1.805-.413 2.227-.228.562-.483-.96-.896 1.383-.42.413-.824-.67-1.382-.896-.422-.167-1.057.36-2.227-.413-1.265.055-1.646.07-4.85.07s-3.585-.015-4.85-.07c-1.17-.055-1.805-.248-2.227-.413-.562-.228-.96-.483-1.382-.896-.413-.42-.67-.824-.896-1.383-.167-.422-.36-1.057-.413-2.227-.055-1.265-.07-1.646-.07-4.85s.015-3.585.07-4.85c.055-1.17.248 1.805.413-2.227.228.562.483.96.896-1.382.42-.413.824-.67 1.382-.896.422-.167 1.057.36 2.227-.413 1.265-.055 1.646-.07 4.85-.07zm0 3.27c-3.405 0-6.167 2.76-6.167 6.167s2.762 6.167 6.167 6.167 6.167-2.76 6.167-6.167-2.762-6.167-6.167-6.167zm0 10.167c-2.209 0-4-1.79-4-4s1.791-4 4-4 4 1.79 4 4-1.791 4-4 4zm4.865-9.865c0 .765-.623 1.385-1.39 1.385s-1.39-.62-1.39-1.385.623-1.385 1.39-1.385 1.39.62 1.39 1.385z" />
  41. </svg>
  42. },
  43. ];
  44. const CheckCircleIcon: React.FC<{ className?: string }> = ({ className }) => (
  45. <Icon className={className || "h-4 w-4 text-green-400"}><path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></Icon>
  46. );
  47. // --- MODAL COMPONENTS ---
  48. const SchedulePostModal: React.FC<{
  49. video: AIGCVideo | null;
  50. post: ScheduledPost | null;
  51. date: string;
  52. accounts: SocialAccount[];
  53. onClose: () => void;
  54. onSchedule: (post: Omit<ScheduledPost, 'id' | 'status'>) => void;
  55. onUpdate: (post: ScheduledPost) => void;
  56. onDelete: (postId: string) => void;
  57. }> = ({ video, post, date, accounts, onClose, onSchedule, onUpdate, onDelete }) => {
  58. const { t, dateLocale } = useTranslation();
  59. const [time, setTime] = React.useState(post ? post.time : '10:00');
  60. const [caption, setCaption] = React.useState(post ? post.caption : (video ? `Check out our new video: ${video.title}!` : ""));
  61. const [selectedAccounts, setSelectedAccounts] = React.useState<string[]>(post ? post.socialAccountIds : []);
  62. const handleAccountToggle = (accountId: string) => {
  63. setSelectedAccounts(prev =>
  64. prev.includes(accountId) ? prev.filter(id => id !== accountId) : [...prev, accountId]
  65. );
  66. };
  67. const handleSubmit = () => {
  68. if (selectedAccounts.length === 0 || !time) {
  69. alert('Please select at least one social account and set a time.');
  70. return;
  71. }
  72. if (post) {
  73. onUpdate({ ...post, time, caption, socialAccountIds: selectedAccounts });
  74. } else if (video) {
  75. onSchedule({ videoId: video.id, date, time, caption, socialAccountIds: selectedAccounts });
  76. }
  77. onClose();
  78. };
  79. if (!video) return null;
  80. return (
  81. <div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50" onClick={() => onClose()}>
  82. <div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md" onClick={e => e.stopPropagation()}>
  83. <div className="p-6 border-b border-gray-200 dark:border-gray-700">
  84. <h3 className="text-lg font-bold text-gray-900 dark:text-white">{post ? t('aigc.scheduler.title') : t('aigc.scheduler.title')}</h3>
  85. <p className="text-sm text-gray-500 dark:text-gray-400">{`${t('for')} ${format(parseISO(date), 'MMMM d, yyyy', { locale: dateLocale })}`}</p>
  86. </div>
  87. <div className="p-6 space-y-4">
  88. <div className="flex items-center gap-4">
  89. <img src={video.thumbnailUrl} alt={video.title} className="w-32 h-20 rounded object-cover" />
  90. <p className="font-semibold text-gray-900 dark:text-white">{video.title}</p>
  91. </div>
  92. <div>
  93. <label htmlFor="time" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{t('aigc.scheduler.time')}</label>
  94. <input type="time" id="time" value={time} onChange={e => setTime(e.target.value)} className="w-full bg-gray-100 dark:bg-gray-700 p-2 rounded-md border border-gray-300 dark:border-gray-600"/>
  95. </div>
  96. <div>
  97. <label htmlFor="caption" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{t('aigc.scheduler.caption')}</label>
  98. <textarea id="caption" rows={3} value={caption} onChange={e => setCaption(e.target.value)} className="w-full bg-gray-100 dark:bg-gray-700 p-2 rounded-md border border-gray-300 dark:border-gray-600" />
  99. </div>
  100. <div>
  101. <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{t('aigc.scheduler.post_to')}</label>
  102. <div className="space-y-2">
  103. {accounts.map(acc => (
  104. <label key={acc.id} className="flex items-center gap-3 p-2 bg-gray-100 dark:bg-gray-700 rounded-md cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600">
  105. <input type="checkbox" className="h-4 w-4 rounded bg-gray-200 dark:bg-gray-900 border-gray-300 dark:border-gray-600 text-brand-primary focus:ring-brand-secondary" checked={selectedAccounts.includes(acc.id)} onChange={() => handleAccountToggle(acc.id)} />
  106. {acc.icon}
  107. <span className="font-semibold">{acc.platform}</span>
  108. <span className="text-gray-500 dark:text-gray-400 text-sm ml-auto">@{acc.username}</span>
  109. </label>
  110. ))}
  111. </div>
  112. </div>
  113. </div>
  114. <div className={`p-4 bg-gray-50 dark:bg-gray-900/50 flex ${post ? 'justify-between' : 'justify-end'} gap-3`}>
  115. {post && <button onClick={() => { onDelete(post.id); onClose(); }} className="px-4 py-2 rounded-md text-sm font-semibold text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/50">{t('delete')}</button>}
  116. <div className="flex gap-3">
  117. <button onClick={onClose} className="px-4 py-2 rounded-md text-sm font-semibold text-gray-700 dark:text-gray-300 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600">{t('cancel')}</button>
  118. <button onClick={handleSubmit} className="px-4 py-2 rounded-md text-sm font-semibold text-white bg-brand-primary hover:bg-brand-secondary">{post ? t('update') : t('aigc.scheduler.schedule')}</button>
  119. </div>
  120. </div>
  121. </div>
  122. </div>
  123. );
  124. };
  125. const VideoLibrarySidebar: React.FC<{
  126. videos: AIGCVideo[];
  127. onDragStart: (e: React.DragEvent, videoId: string) => void;
  128. activeFilter: 'all' | 'scheduled' | 'unscheduled';
  129. onFilterChange: (filter: 'all' | 'scheduled' | 'unscheduled') => void;
  130. }> = ({ videos, onDragStart, activeFilter, onFilterChange }) => {
  131. const { t } = useTranslation();
  132. return (
  133. <aside className="w-80 bg-white dark:bg-gray-800 p-4 flex flex-col border-r border-gray-200 dark:border-gray-700">
  134. <h3 className="text-lg font-bold text-gray-900 dark:text-white mb-4">{t('aigc.scheduler.video_library')}</h3>
  135. <div className="flex items-center gap-1 bg-gray-100 dark:bg-gray-900 p-1 rounded-lg mb-4">
  136. {(['all', 'scheduled', 'unscheduled'] as const).map(filter => (
  137. <button
  138. key={filter}
  139. onClick={() => onFilterChange(filter)}
  140. className={`flex-1 px-3 py-1 text-sm rounded-md capitalize transition-colors ${activeFilter === filter ? 'bg-brand-primary text-white shadow' : 'hover:bg-gray-200 dark:hover:bg-gray-600'}`}>
  141. {t(filter)}
  142. </button>
  143. ))}
  144. </div>
  145. <div className="flex-1 overflow-y-auto pr-2 -mr-4 space-y-3">
  146. {videos.map(video => (
  147. <div key={video.id} draggable onDragStart={e => onDragStart(e, video.id)} className="flex items-center gap-3 p-2 bg-gray-50 dark:bg-gray-900/50 rounded-lg cursor-grab active:cursor-grabbing">
  148. <img src={video.thumbnailUrl} alt={video.title} className="w-24 h-16 rounded object-cover" />
  149. <p className="flex-1 font-semibold text-sm text-gray-900 dark:text-white truncate">{video.title}</p>
  150. </div>
  151. ))}
  152. {videos.length === 0 && <p className="text-center text-sm text-gray-400 dark:text-gray-500 py-10">{t('aigc.scheduler.no_videos_filter')}</p>}
  153. </div>
  154. </aside>
  155. );
  156. };
  157. // --- CALENDAR COMPONENTS ---
  158. const CalendarHeader: React.FC<{ view: string; currentDate: Date; onPrev: () => void; onNext: () => void; onViewChange: (view: 'month' | 'week' | 'day') => void; }> = ({ view, currentDate, onPrev, onNext, onViewChange }) => {
  159. const { dateLocale } = useTranslation();
  160. const formatString = view === 'month' ? 'MMMM yyyy' : (view === 'week' ? "MMM d, yyyy" : 'MMMM d, yyyy');
  161. return (
  162. <div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
  163. <div>
  164. <h2 className="text-xl font-bold text-gray-900 dark:text-white">{format(currentDate, formatString, { locale: dateLocale })}</h2>
  165. {view === 'week' && <p className="text-sm text-gray-500 dark:text-gray-400">{`Week of ${format(startOfWeek(currentDate, { locale: dateLocale }), 'MMM d')} - ${format(endOfWeek(currentDate, { locale: dateLocale }), 'MMM d')}`}</p>}
  166. </div>
  167. <div className="flex items-center gap-4">
  168. <div className="flex items-center">
  169. <button onClick={onPrev} className="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"><Icon className="h-5 w-5"><path d="M15 19l-7-7 7-7" /></Icon></button>
  170. <button onClick={onNext} className="p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"><Icon className="h-5 w-5"><path d="M9 5l7 7-7 7" /></Icon></button>
  171. </div>
  172. <div className="flex items-center gap-1 bg-gray-200 dark:bg-gray-700 p-1 rounded-lg">
  173. {(['month', 'week', 'day'] as const).map(v => (
  174. <button key={v} onClick={() => onViewChange(v)} className={`px-3 py-1 text-sm rounded-md capitalize ${view === v ? 'bg-brand-primary text-white' : 'hover:bg-gray-300 dark:hover:bg-gray-600'}`}>{v}</button>
  175. ))}
  176. </div>
  177. </div>
  178. </div>
  179. );
  180. };
  181. const ScheduledPostItem: React.FC<{ post: ScheduledPost; video: AIGCVideo | undefined; onClick: (e: React.MouseEvent) => void; }> = ({ post, video, onClick }) => {
  182. if (!video) return null;
  183. const postDate = parseISO(`${post.date}T${post.time}`);
  184. const isDistributed = post.status === 'distributed' || isPast(postDate);
  185. return (
  186. <button onClick={onClick} className={`w-full text-left p-2 rounded-md mb-1 ${isDistributed ? 'bg-green-500/10 hover:bg-green-500/20' : 'bg-blue-500/10 hover:bg-blue-500/20'}`}>
  187. <div className="flex items-center gap-2">
  188. {isDistributed ? <CheckCircleIcon className="h-4 w-4 text-green-400" /> : <div className="h-4 w-4" />}
  189. <p className={`text-xs font-semibold ${isDistributed ? 'text-green-600 dark:text-green-400' : 'text-blue-600 dark:text-blue-400'}`}>{post.time}</p>
  190. </div>
  191. <p className="text-sm font-semibold truncate mt-1 text-gray-800 dark:text-gray-200">{video.title}</p>
  192. </button>
  193. );
  194. };
  195. const Calendar: React.FC<{
  196. view: 'month' | 'week' | 'day';
  197. currentDate: Date;
  198. schedule: ScheduledPost[];
  199. videos: AIGCVideo[];
  200. onDateSelect: (date: string) => void;
  201. onPostSelect: (post: ScheduledPost) => void;
  202. onDrop: (e: React.DragEvent, date: string) => void;
  203. }> = ({ view, currentDate, schedule, videos, onDateSelect, onPostSelect, onDrop }) => {
  204. const { dateLocale } = useTranslation();
  205. const [dragOverDate, setDragOverDate] = React.useState<string | null>(null);
  206. const renderMonthView = () => {
  207. const monthStart = startOfMonth(currentDate);
  208. const monthEnd = endOfMonth(monthStart);
  209. const days = eachDayOfInterval({ start: startOfWeek(monthStart, { locale: dateLocale }), end: endOfWeek(monthEnd, { locale: dateLocale }) });
  210. const weekdays = eachDayOfInterval({
  211. start: startOfWeek(currentDate, { locale: dateLocale }),
  212. end: endOfWeek(currentDate, { locale: dateLocale }),
  213. });
  214. return (
  215. <>
  216. <div className="grid grid-cols-7 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
  217. {weekdays.map(day => <div key={day.toString()} className="py-2">{format(day, 'E', { locale: dateLocale })}</div>)}
  218. </div>
  219. <div className="grid grid-cols-7 flex-1">
  220. {days.map(day => {
  221. const dayStr = format(day, 'yyyy-MM-dd');
  222. const isDragOver = dragOverDate === dayStr;
  223. const postsForDay = schedule.filter(p => isSameDay(parseISO(p.date), day)).sort((a,b) => a.time.localeCompare(b.time));
  224. return (
  225. <div
  226. key={day.toString()}
  227. onDragEnter={(e) => { e.preventDefault(); setDragOverDate(dayStr); }}
  228. onDragLeave={(e) => { e.preventDefault(); setDragOverDate(null); }}
  229. onDragOver={e => e.preventDefault()}
  230. onDrop={e => { onDrop(e, dayStr); setDragOverDate(null); }}
  231. onClick={() => onDateSelect(dayStr)}
  232. className={`relative border-r border-b border-gray-200 dark:border-gray-700 p-2 transition-all duration-200 ${!isSameMonth(day, currentDate) ? 'bg-gray-50 dark:bg-gray-900/50' : ''} ${isDragOver ? 'bg-brand-primary/20 border-2 border-brand-primary' : ''}`}
  233. >
  234. <time dateTime={dayStr} className={`text-sm font-semibold ${isToday(day) ? 'bg-brand-primary text-white rounded-full h-6 w-6 flex items-center justify-center' : ''}`}>{format(day, 'd')}</time>
  235. <div className="mt-2">{postsForDay.map(post => <ScheduledPostItem key={post.id} post={post} video={videos.find(v => v.id === post.videoId)} onClick={e => { e.stopPropagation(); onPostSelect(post); }} />)}</div>
  236. </div>
  237. );
  238. })}
  239. </div>
  240. </>
  241. );
  242. };
  243. // Day and Week views could be implemented similarly if needed
  244. return (
  245. <div className="flex-1 flex flex-col overflow-y-auto">
  246. {renderMonthView()}
  247. </div>
  248. );
  249. };
  250. // --- MAIN COMPONENT ---
  251. interface ContentSchedulerProps {
  252. videos: AIGCVideo[];
  253. schedule: ScheduledPost[];
  254. onScheduleUpdate: (newSchedule: ScheduledPost[]) => void;
  255. }
  256. export const ContentScheduler: React.FC<ContentSchedulerProps> = ({ videos, schedule, onScheduleUpdate }) => {
  257. const [currentDate, setCurrentDate] = React.useState(new Date());
  258. const [view, setView] = React.useState<'month' | 'week' | 'day'>('month');
  259. const [isModalOpen, setIsModalOpen] = React.useState(false);
  260. const [modalData, setModalData] = React.useState<{ videoId: string | null; postId: string | null; date: string }>({ videoId: null, postId: null, date: '' });
  261. const [videoFilter, setVideoFilter] = React.useState<'all' | 'scheduled' | 'unscheduled'>('all');
  262. const scheduledVideoIds = React.useMemo(() => new Set(schedule.map(p => p.videoId)), [schedule]);
  263. const filteredVideos = React.useMemo(() => {
  264. if (videoFilter === 'scheduled') {
  265. return videos.filter(v => scheduledVideoIds.has(v.id));
  266. }
  267. if (videoFilter === 'unscheduled') {
  268. return videos.filter(v => !scheduledVideoIds.has(v.id));
  269. }
  270. return videos;
  271. }, [videos, videoFilter, scheduledVideoIds]);
  272. const handleSchedule = (newPostData: Omit<ScheduledPost, 'id' | 'status'>) => {
  273. const newPost: ScheduledPost = { ...newPostData, id: `post-${Date.now()}`, status: 'scheduled' };
  274. onScheduleUpdate([...schedule, newPost]);
  275. };
  276. const handleUpdate = (updatedPost: ScheduledPost) => onScheduleUpdate(schedule.map(p => p.id === updatedPost.id ? updatedPost : p));
  277. const handleDelete = (postId: string) => onScheduleUpdate(schedule.filter(p => p.id !== postId));
  278. const handleDragStart = (e: React.DragEvent, videoId: string) => e.dataTransfer.setData("videoId", videoId);
  279. const handleDrop = (e: React.DragEvent, date: string) => {
  280. const videoId = e.dataTransfer.getData("videoId");
  281. if(videoId) {
  282. setModalData({ videoId, postId: null, date });
  283. setIsModalOpen(true);
  284. }
  285. };
  286. const changeDate = (direction: 'prev' | 'next') => {
  287. const op = direction === 'prev' ? 'sub' : 'add';
  288. const unit = view === 'month' ? 'Months' : view === 'week' ? 'Weeks' : 'Days';
  289. const fn = {
  290. sub: { Months: subMonths, Weeks: subWeeks, Days: subDays },
  291. add: { Months: addMonths, Weeks: addWeeks, Days: addDays }
  292. }[op][unit];
  293. setCurrentDate(fn(currentDate, 1));
  294. };
  295. const modalPost = modalData.postId ? schedule.find(p => p.id === modalData.postId) : null;
  296. const modalVideo = videos.find(v => v.id === (modalPost ? modalPost.videoId : modalData.videoId));
  297. return (
  298. <div className="flex h-full">
  299. {isModalOpen && <SchedulePostModal
  300. video={modalVideo || null}
  301. post={modalPost || null}
  302. date={modalData.date}
  303. accounts={initialSocialAccounts}
  304. onClose={() => setIsModalOpen(false)}
  305. onSchedule={handleSchedule}
  306. onUpdate={handleUpdate}
  307. onDelete={handleDelete}
  308. />}
  309. <VideoLibrarySidebar
  310. videos={filteredVideos}
  311. onDragStart={handleDragStart}
  312. activeFilter={videoFilter}
  313. onFilterChange={setVideoFilter}
  314. />
  315. <main className="flex-1 flex flex-col bg-white dark:bg-gray-800/50">
  316. <CalendarHeader
  317. view={view}
  318. currentDate={currentDate}
  319. onPrev={() => changeDate('prev')}
  320. onNext={() => changeDate('next')}
  321. onViewChange={setView}
  322. />
  323. <Calendar
  324. view={view}
  325. currentDate={currentDate}
  326. schedule={schedule}
  327. videos={videos}
  328. onDateSelect={(date) => { setModalData({ videoId: null, postId: null, date }); setIsModalOpen(true); }}
  329. onPostSelect={post => { setModalData({ videoId: null, postId: post.id, date: post.date }); setIsModalOpen(true); }}
  330. onDrop={handleDrop}
  331. />
  332. </main>
  333. </div>
  334. );
  335. };