| 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> |
| ); |
| } |
|
|