PostGen / frontend /src /pages /Scheduler.jsx
Seth
update
f80e9b3
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Calendar as CalendarIcon,
ChevronLeft,
ChevronRight,
Plus,
Clock,
Layers,
Image,
FileText,
Video,
Filter,
Settings,
Sparkles,
Check,
X,
GripVertical
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Calendar } from '@/components/ui/calendar';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import { Slider } from '@/components/ui/slider';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { format, addDays, startOfWeek, endOfWeek, eachDayOfInterval, isSameDay, isToday } from 'date-fns';
import { Link } from 'react-router-dom';
import { createPageUrl } from '@/utils';
const products = [
{ id: 'ocr', name: 'Document Parsing (OCR)', shortName: 'OCR', color: 'blue' },
{ id: 'p2p', name: 'Purchase To Pay', shortName: 'P2P', color: 'emerald', subCategories: ['Budget Approval', 'Purchase Request', 'Accounts Payable'] },
{ id: 'o2c', name: 'Order to Cash', shortName: 'O2C', color: 'violet', subCategories: ['Quotation', 'Sales Order', 'PickSlip Delivery', 'Accounts Receivable'] },
];
const postTypes = [
{ id: 'carousel', name: 'Carousel', icon: Layers, description: 'Multi-slide visual story' },
{ id: 'cover_content', name: 'Cover Image + Content', icon: Image, description: 'Featured image with text' },
{ id: 'content_only', name: 'Content Only', icon: FileText, description: 'Text-based post' },
{ id: 'webinar', name: 'Webinar Invite', icon: Video, description: 'Event promotion' },
];
const scheduledPosts = [
{ id: 1, title: 'OCR Automation Benefits', type: 'carousel', product: 'ocr', time: '10:00 AM', date: new Date(), status: 'scheduled' },
{ id: 2, title: 'P2P Efficiency Guide', type: 'cover_content', product: 'p2p', time: '2:00 PM', date: new Date(), status: 'draft' },
{ id: 3, title: 'O2C Webinar', type: 'webinar', product: 'o2c', time: '11:00 AM', date: addDays(new Date(), 1), status: 'scheduled' },
{ id: 4, title: 'Invoice Processing Tips', type: 'content_only', product: 'ocr', time: '3:00 PM', date: addDays(new Date(), 1), status: 'scheduled' },
{ id: 5, title: 'Budget Approval Flow', type: 'carousel', product: 'p2p', time: '9:00 AM', date: addDays(new Date(), 2), status: 'scheduled' },
{ id: 6, title: 'Sales Order Demo', type: 'cover_content', product: 'o2c', time: '1:00 PM', date: addDays(new Date(), 3), status: 'draft' },
];
export default function Scheduler() {
const [selectedDate, setSelectedDate] = useState(new Date());
const [currentWeekStart, setCurrentWeekStart] = useState(startOfWeek(new Date(), { weekStartsOn: 1 }));
const [campaignDialogOpen, setCampaignDialogOpen] = useState(false);
const [selectedProducts, setSelectedProducts] = useState(['ocr', 'p2p', 'o2c']);
const [selectedPostTypes, setSelectedPostTypes] = useState(['carousel', 'cover_content']);
const [dateRange, setDateRange] = useState({ from: new Date(), to: addDays(new Date(), 30) });
const [postsPerWeek, setPostsPerWeek] = useState([5]);
const weekDays = eachDayOfInterval({
start: currentWeekStart,
end: endOfWeek(currentWeekStart, { weekStartsOn: 1 })
});
const navigateWeek = (direction) => {
setCurrentWeekStart(prev => addDays(prev, direction === 'next' ? 7 : -7));
};
const getPostsForDate = (date) => {
return scheduledPosts.filter(post => isSameDay(post.date, date));
};
const getProductColor = (productId) => {
const product = products.find(p => p.id === productId);
return product?.color || 'slate';
};
const getPostTypeIcon = (typeId) => {
const type = postTypes.find(t => t.id === typeId);
return type?.icon || FileText;
};
const toggleProduct = (productId) => {
setSelectedProducts(prev =>
prev.includes(productId)
? prev.filter(id => id !== productId)
: [...prev, productId]
);
};
const togglePostType = (typeId) => {
setSelectedPostTypes(prev =>
prev.includes(typeId)
? prev.filter(id => id !== typeId)
: [...prev, typeId]
);
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50/30">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-slate-900 tracking-tight">
Content Scheduler
</h1>
<p className="text-slate-500 mt-1">
Plan and schedule your LinkedIn content calendar
</p>
</div>
<div className="flex gap-3">
<Dialog open={campaignDialogOpen} onOpenChange={setCampaignDialogOpen}>
<DialogTrigger asChild>
<Button variant="outline" className="gap-2 border-slate-200">
<Settings className="w-4 h-4" />
Campaign Settings
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Sparkles className="w-5 h-5 text-amber-500" />
Configure Campaign
</DialogTitle>
</DialogHeader>
<div className="space-y-6 pt-4">
{/* Date Range */}
<div className="space-y-3">
<Label className="text-sm font-semibold">Campaign Date Range</Label>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-xs text-slate-500">Start Date</Label>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full justify-start mt-1.5">
<CalendarIcon className="w-4 h-4 mr-2" />
{format(dateRange.from, 'MMM d, yyyy')}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={dateRange.from}
onSelect={(date) => setDateRange(prev => ({ ...prev, from: date }))}
/>
</PopoverContent>
</Popover>
</div>
<div>
<Label className="text-xs text-slate-500">End Date</Label>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full justify-start mt-1.5">
<CalendarIcon className="w-4 h-4 mr-2" />
{format(dateRange.to, 'MMM d, yyyy')}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={dateRange.to}
onSelect={(date) => setDateRange(prev => ({ ...prev, to: date }))}
/>
</PopoverContent>
</Popover>
</div>
</div>
</div>
{/* Products to Focus */}
<div className="space-y-3">
<Label className="text-sm font-semibold">Products to Focus</Label>
<div className="grid gap-3">
{products.map(product => (
<div key={product.id} className="space-y-2">
<div
onClick={() => toggleProduct(product.id)}
className={`flex items-center gap-3 p-3 rounded-xl border cursor-pointer transition-all ${
selectedProducts.includes(product.id)
? `border-${product.color}-300 bg-${product.color}-50`
: 'border-slate-200 hover:border-slate-300'
}`}
>
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
selectedProducts.includes(product.id)
? `bg-${product.color}-500 text-white`
: 'bg-slate-100 text-slate-400'
}`}>
{selectedProducts.includes(product.id) ? (
<Check className="w-4 h-4" />
) : (
<Plus className="w-4 h-4" />
)}
</div>
<div className="flex-1">
<p className="font-medium text-slate-900">{product.name}</p>
{product.subCategories && (
<p className="text-xs text-slate-500 mt-0.5">
{product.subCategories.join(', ')}
</p>
)}
</div>
</div>
</div>
))}
</div>
</div>
{/* Post Types Mix */}
<div className="space-y-3">
<Label className="text-sm font-semibold">Post Types Mix</Label>
<div className="grid grid-cols-2 gap-3">
{postTypes.map(type => (
<div
key={type.id}
onClick={() => togglePostType(type.id)}
className={`flex items-center gap-3 p-3 rounded-xl border cursor-pointer transition-all ${
selectedPostTypes.includes(type.id)
? 'border-blue-300 bg-blue-50'
: 'border-slate-200 hover:border-slate-300'
}`}
>
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
selectedPostTypes.includes(type.id)
? 'bg-blue-500 text-white'
: 'bg-slate-100 text-slate-400'
}`}>
<type.icon className="w-5 h-5" />
</div>
<div>
<p className="font-medium text-sm text-slate-900">{type.name}</p>
<p className="text-xs text-slate-500">{type.description}</p>
</div>
</div>
))}
</div>
</div>
{/* Posting Frequency */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-sm font-semibold">Posts per Week</Label>
<span className="text-lg font-bold text-blue-600">{postsPerWeek[0]}</span>
</div>
<Slider
value={postsPerWeek}
onValueChange={setPostsPerWeek}
min={1}
max={14}
step={1}
className="py-4"
/>
<div className="flex justify-between text-xs text-slate-500">
<span>1 post</span>
<span>14 posts</span>
</div>
</div>
<div className="flex justify-end gap-2 pt-4 border-t">
<Button variant="outline" onClick={() => setCampaignDialogOpen(false)}>
Cancel
</Button>
<Button className="bg-gradient-to-r from-blue-600 to-indigo-600 gap-2">
<Sparkles className="w-4 h-4" />
Generate Schedule
</Button>
</div>
</div>
</DialogContent>
</Dialog>
<Link to={createPageUrl('PostEditor')}>
<Button className="gap-2 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-lg shadow-blue-500/25">
<Plus className="w-4 h-4" />
Create Post
</Button>
</Link>
</div>
</div>
</motion.div>
<div className="grid lg:grid-cols-4 gap-6">
{/* Calendar Sidebar */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="lg:col-span-1 space-y-6"
>
<Card className="border-0 shadow-lg shadow-slate-200/50">
<CardContent className="p-4">
<Calendar
mode="single"
selected={selectedDate}
onSelect={setSelectedDate}
className="rounded-md"
/>
</CardContent>
</Card>
{/* Quick Stats */}
<Card className="border-0 shadow-lg shadow-slate-200/50">
<CardHeader className="pb-3">
<CardTitle className="text-sm font-semibold text-slate-600">This Week</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600">Scheduled</span>
<Badge className="bg-blue-100 text-blue-700 border-0">12 posts</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600">Drafts</span>
<Badge className="bg-amber-100 text-amber-700 border-0">4 posts</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600">Published</span>
<Badge className="bg-emerald-100 text-emerald-700 border-0">8 posts</Badge>
</div>
</CardContent>
</Card>
{/* Product Filter */}
<Card className="border-0 shadow-lg shadow-slate-200/50">
<CardHeader className="pb-3">
<CardTitle className="text-sm font-semibold text-slate-600 flex items-center gap-2">
<Filter className="w-4 h-4" />
Filter by Product
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{products.map(product => (
<label key={product.id} className="flex items-center gap-3 cursor-pointer">
<Checkbox defaultChecked />
<span className="text-sm text-slate-700">{product.shortName}</span>
<div className={`w-2 h-2 rounded-full bg-${product.color}-500 ml-auto`} />
</label>
))}
</CardContent>
</Card>
</motion.div>
{/* Week View */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="lg:col-span-3"
>
<Card className="border-0 shadow-lg shadow-slate-200/50">
<CardHeader className="pb-4 border-b border-slate-100">
<div className="flex items-center justify-between">
<CardTitle className="text-lg font-semibold">
{format(currentWeekStart, 'MMMM yyyy')}
</CardTitle>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
onClick={() => navigateWeek('prev')}
className="h-8 w-8"
>
<ChevronLeft className="w-4 h-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentWeekStart(startOfWeek(new Date(), { weekStartsOn: 1 }))}
>
Today
</Button>
<Button
variant="outline"
size="icon"
onClick={() => navigateWeek('next')}
className="h-8 w-8"
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="grid grid-cols-7 border-b border-slate-100">
{weekDays.map((day, index) => (
<div
key={index}
className={`text-center py-4 border-r last:border-r-0 border-slate-100 ${
isToday(day) ? 'bg-blue-50' : ''
}`}
>
<p className="text-xs text-slate-500 uppercase">
{format(day, 'EEE')}
</p>
<p className={`text-lg font-semibold mt-1 ${
isToday(day) ? 'text-blue-600' : 'text-slate-900'
}`}>
{format(day, 'd')}
</p>
</div>
))}
</div>
<div className="grid grid-cols-7 min-h-[400px]">
{weekDays.map((day, dayIndex) => {
const dayPosts = getPostsForDate(day);
return (
<div
key={dayIndex}
className={`border-r last:border-r-0 border-slate-100 p-2 ${
isToday(day) ? 'bg-blue-50/30' : ''
}`}
>
<AnimatePresence>
{dayPosts.map((post, postIndex) => {
const PostIcon = getPostTypeIcon(post.type);
const color = getProductColor(post.product);
return (
<motion.div
key={post.id}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ delay: postIndex * 0.05 }}
>
<Link to={createPageUrl('PostEditor') + `?id=${post.id}`}>
<div className={`mb-2 p-2 rounded-lg bg-white border border-${color}-200 hover:border-${color}-300 shadow-sm hover:shadow-md transition-all cursor-pointer group`}>
<div className="flex items-center gap-1.5 mb-1">
<div className={`w-5 h-5 rounded flex items-center justify-center bg-${color}-100`}>
<PostIcon className={`w-3 h-3 text-${color}-600`} />
</div>
<span className="text-[10px] text-slate-500">{post.time}</span>
</div>
<p className="text-xs font-medium text-slate-800 line-clamp-2 leading-tight">
{post.title}
</p>
<div className="flex items-center justify-between mt-1.5">
<Badge className={`text-[9px] px-1 py-0 bg-${color}-100 text-${color}-700 border-0`}>
{products.find(p => p.id === post.product)?.shortName}
</Badge>
{post.status === 'draft' && (
<span className="text-[9px] text-amber-600">Draft</span>
)}
</div>
</div>
</Link>
</motion.div>
);
})}
</AnimatePresence>
<Button
variant="ghost"
size="sm"
className="w-full h-7 text-xs text-slate-400 hover:text-slate-600 opacity-0 hover:opacity-100"
>
<Plus className="w-3 h-3 mr-1" />
Add
</Button>
</div>
);
})}
</div>
</CardContent>
</Card>
</motion.div>
</div>
</div>
</div>
);
}