Zero Block
Click "Block Editor" to enter the edit mode. Use layers, shapes and customize adaptability. Everything is in your hands.
Tilda Publishing
import React, { useState } from 'react';
import { Calendar, Plus, Edit2, Trash2, CheckCircle, Clock, AlertCircle } from 'lucide-react';

const EditorialCalendar = () => {
const [currentDate, setCurrentDate] = useState(new Date());
const [projects, setProjects] = useState({});
const [showAddForm, setShowAddForm] = useState(false);
const [editingProject, setEditingProject] = useState(null);
const [newProject, setNewProject] = useState({
title: '',
description: '',
status: 'planning',
priority: 'medium',
type: 'blog'
});

const months = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];

const contentTypes = ['blog', 'social', 'newsletter', 'video', 'podcast', 'case-study', 'update'];
const statuses = ['planning', 'rendering', 'drawings', 'construction'];
const priorities = ['low', 'medium', 'high'];

const getStatusColor = (status) => {
switch(status) {
case 'planning': return 'bg-blue-100 text-blue-800';
case 'rendering': return 'bg-yellow-100 text-yellow-800';
case 'drawings': return 'bg-orange-100 text-orange-800';
case 'construction': return 'bg-green-100 text-green-800';
default: return 'bg-gray-100 text-gray-800';
}
};

const getPriorityIcon = (priority) => {
switch(priority) {
case 'high': return <AlertCircle className="w-4 h-4 text-red-500" />;
case 'medium': return <Clock className="w-4 h-4 text-yellow-500" />;
case 'low': return <CheckCircle className="w-4 h-4 text-green-500" />;
default: return null;
}
};

const getDaysInMonth = (year, month) => {
return new Date(year, month + 1, 0).getDate();
};

const getFirstDayOfMonth = (year, month) => {
const firstDay = new Date(year, month, 1).getDay();
return firstDay === 0 ? 6 : firstDay - 1; // Convert Sunday (0) to 6, and shift others
};

const changeMonth = (direction) => {
const newDate = new Date(currentDate);
newDate.setMonth(currentDate.getMonth() + direction);
setCurrentDate(newDate);
};

const addProject = (day) => {
const dateKey = `${currentDate.getFullYear()}-${currentDate.getMonth()}-${day}`;
if (!projects[dateKey]) {
projects[dateKey] = [];
}
projects[dateKey].push({ ...newProject, id: Date.now() });
setProjects({ ...projects });
setNewProject({ title: '', description: '', status: 'planning', priority: 'medium', type: 'blog' });
setShowAddForm(false);
};

const deleteProject = (dateKey, projectId) => {
const updatedProjects = { ...projects };
updatedProjects[dateKey] = updatedProjects[dateKey].filter(p => p.id !== projectId);
if (updatedProjects[dateKey].length === 0) {
delete updatedProjects[dateKey];
}
setProjects(updatedProjects);
};

const updateProject = (dateKey, projectId, updates) => {
const updatedProjects = { ...projects };
const projectIndex = updatedProjects[dateKey].findIndex(p => p.id === projectId);
updatedProjects[dateKey][projectIndex] = { ...updatedProjects[dateKey][projectIndex], ...updates };
setProjects(updatedProjects);
setEditingProject(null);
};

const renderCalendarDays = () => {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const daysInMonth = getDaysInMonth(year, month);
const firstDay = getFirstDayOfMonth(year, month);
const days = [];

// Empty cells for days before the first day of the month
for (let i = 0; i < firstDay; i++) {
days.push(<div key={`empty-${i}`} className="h-32 border border-gray-200"></div>);
}

// Days of the month
for (let day = 1; day <= daysInMonth; day++) {
const dateKey = `${year}-${month}-${day}`;
const dayProjects = projects[dateKey] || [];

days.push(
<div key={day} className="h-32 border border-gray-200 p-2 overflow-y-auto">
<div className="flex justify-between items-center mb-1">
<span className="text-sm font-medium">{day}</span>
<button
onClick={() => setShowAddForm(day)}
className="w-5 h-5 flex items-center justify-center text-gray-400 hover:text-blue-500 hover:bg-blue-50 rounded"
>
<Plus className="w-3 h-3" />
</button>
</div>

{dayProjects.map((project) => (
<div key={project.id} className="mb-1">
<div className={`text-xs px-2 py-1 rounded-full ${getStatusColor(project.status)} flex items-center justify-between`}>
<div className="flex items-center gap-1">
{getPriorityIcon(project.priority)}
<span className="font-medium truncate">{project.title}</span>
</div>
<div className="flex gap-1">
<button
onClick={() => setEditingProject({ dateKey, project })}
className="hover:bg-white hover:bg-opacity-50 rounded p-0.5"
>
<Edit2 className="w-3 h-3" />
</button>
<button
onClick={() => deleteProject(dateKey, project.id)}
className="hover:bg-white hover:bg-opacity-50 rounded p-0.5"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
</div>
<div className="text-xs text-gray-600 mt-1">
<span className="bg-gray-100 px-1 rounded">{project.type}</span>
</div>
</div>
))}
</div>
);
}

return days;
};

return (
<div className="max-w-6xl mx-auto p-6 bg-white">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<Calendar className="w-8 h-8 text-blue-600" />
<h1 className="text-2xl font-bold text-gray-900">Project Editorial Calendar</h1>
</div>
<div className="flex items-center gap-4">
<button
onClick={() => changeMonth(-1)}
className="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg"
>

</button>
<span className="text-lg font-semibold min-w-[140px] text-center">
{months[currentDate.getMonth()]} {currentDate.getFullYear()}
</span>
<button
onClick={() => changeMonth(1)}
className="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-lg"
>

</button>
</div>
</div>

{/* Legend */}
<div className="mb-4 p-4 bg-gray-50 rounded-lg">
<h3 className="text-sm font-medium mb-2">Status Legend:</h3>
<div className="flex gap-4 text-xs">
{statuses.map(status => (
<span key={status} className={`px-2 py-1 rounded-full ${getStatusColor(status)}`}>
{status}
</span>
))}
</div>
</div>

{/* Calendar Grid */}
<div className="grid grid-cols-7 gap-0 border border-gray-200 rounded-lg overflow-hidden">
{['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map(day => (
<div key={day} className="bg-gray-100 p-3 text-center font-medium text-gray-700 border-b border-gray-200">
{day}
</div>
))}
{renderCalendarDays()}
</div>

{/* Add Project Modal */}
{showAddForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-md">
<h3 className="text-lg font-semibold mb-4">Add Project for Day {showAddForm}</h3>

<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Project Title</label>
<input
type="text"
value={newProject.title}
onChange={(e) => setNewProject({...newProject, title: e.target.value})}
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Enter project title"
/>
</div>

<div>
<label className="block text-sm font-medium mb-1">Description</label>
<textarea
value={newProject.description}
onChange={(e) => setNewProject({...newProject, description: e.target.value})}
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
rows="3"
placeholder="Brief description"
/>
</div>

<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Type</label>
<select
value={newProject.type}
onChange={(e) => setNewProject({...newProject, type: e.target.value})}
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
{contentTypes.map(type => (
<option key={type} value={type}>{type}</option>
))}
</select>
</div>

<div>
<label className="block text-sm font-medium mb-1">Status</label>
<select
value={newProject.status}
onChange={(e) => setNewProject({...newProject, status: e.target.value})}
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
{statuses.map(status => (
<option key={status} value={status}>{status}</option>
))}
</select>
</div>

<div>
<label className="block text-sm font-medium mb-1">Priority</label>
<select
value={newProject.priority}
onChange={(e) => setNewProject({...newProject, priority: e.target.value})}
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
{priorities.map(priority => (
<option key={priority} value={priority}>{priority}</option>
))}
</select>
</div>
</div>
</div>

<div className="flex justify-end gap-3 mt-6">
<button
onClick={() => setShowAddForm(false)}
className="px-4 py-2 text-gray-600 hover:text-gray-800"
>
Cancel
</button>
<button
onClick={() => addProject(showAddForm)}
disabled={!newProject.title}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-300"
>
Add Project
</button>
</div>
</div>
</div>
)}

{/* Edit Project Modal */}
{editingProject && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-md">
<h3 className="text-lg font-semibold mb-4">Edit Project</h3>

<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Project Title</label>
<input
type="text"
value={editingProject.project.title}
onChange={(e) => setEditingProject({
...editingProject,
project: {...editingProject.project, title: e.target.value}
})}
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>

<div>
<label className="block text-sm font-medium mb-1">Description</label>
<textarea
value={editingProject.project.description}
onChange={(e) => setEditingProject({
...editingProject,
project: {...editingProject.project, description: e.target.value}
})}
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
rows="3"
/>
</div>

<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Type</label>
<select
value={editingProject.project.type}
onChange={(e) => setEditingProject({
...editingProject,
project: {...editingProject.project, type: e.target.value}
})}
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
{contentTypes.map(type => (
<option key={type} value={type}>{type}</option>
))}
</select>
</div>

<div>
<label className="block text-sm font-medium mb-1">Status</label>
<select
value={editingProject.project.status}
onChange={(e) => setEditingProject({
...editingProject,
project: {...editingProject.project, status: e.target.value}
})}
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
{statuses.map(status => (
<option key={status} value={status}>{status}</option>
))}
</select>
</div>

<div>
<label className="block text-sm font-medium mb-1">Priority</label>
<select
value={editingProject.project.priority}
onChange={(e) => setEditingProject({
...editingProject,
project: {...editingProject.project, priority: e.target.value}
})}
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
{priorities.map(priority => (
<option key={priority} value={priority}>{priority}</option>
))}
</select>
</div>
</div>
</div>

<div className="flex justify-end gap-3 mt-6">
<button
onClick={() => setEditingProject(null)}
className="px-4 py-2 text-gray-600 hover:text-gray-800"
>
Cancel
</button>
<button
onClick={() => updateProject(editingProject.dateKey, editingProject.project.id, editingProject.project)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Update Project
</button>
</div>
</div>
</div>
)}
</div>
);
};

export default EditorialCalendar;create your own block from scratch