A
Anonymous

Gantt Chart - Copy this React, Tailwind Component to your project

Add the functionality that the parent task to show up in the Task column as a group (For example project planning could be the parent task and design phase and development the children) here is the code to modify: "use client"; import React, { useState, useEffect } from "react"; import { FiEdit2, FiTrash2, FiPlus, FiCheck, FiX } from "react icons/fi"; import { format, addDays, differenceInDays, min, max, isWithinInterval, startOfWeek, getWeek } from "date fns"; const colorOptions = [ "bg blue 500", "bg green 500", "bg purple 500", "bg red 500", "bg yellow 500", "bg pink 500", "bg indigo 500", "bg teal 500", "bg orange 500" ]; // Fonction pour obtenir une couleur aléatoire const getRandomColor = () => { return colorOptions[Math.floor(Math.random() * colorOptions.length)]; }; // Fonction pour calculer le nombre total de jours entre la date de début la plus ancienne et la date de fin la plus éloignée const calculateTotalDays = (tasks) => { if (tasks.length === 0) return 0; // Trouver la date de début la plus ancienne et la date de fin la plus éloignée const startDates = tasks.map(task => task.startDate); const endDates = tasks.map(task => task.endDate); const earliestStartDate = min(startDates); const latestEndDate = max(endDates); // Calculer le nombre total de jours entre ces deux dates return differenceInDays(latestEndDate, earliestStartDate) + 1; // +1 pour inclure le dernier jour }; const GanttChart = () => { const [tasks, setTasks] = useState([ { id: 1, name: "Project Planning", startDate: new Date(2024, 0, 1), endDate: new Date(2024, 0, 15), progress: 75, color: "bg blue 500", days: 15, parentId: null, customFields: {} }, { id: 2, name: "Design Phase", startDate: new Date(2024, 0, 10), endDate: new Date(2024, 0, 25), progress: 60, color: "bg green 500", days: 15, parentId: 1, customFields: {} }, { id: 3, name: "Development", startDate: new Date(2024, 0, 20), endDate: new Date(2024, 1, 15), progress: 40, color: "bg purple 500", days: 25, parentId: null, customFields: {} } ]); const [customColumns, setCustomColumns] = useState([]); const [editMode, setEditMode] = useState(false); const [editingTask, setEditingTask] = useState(null); const [error, setError] = useState(""); const [showColumnModal, setShowColumnModal] = useState(false); const [newColumn, setNewColumn] = useState({ name: "", type: "text" }); const totalDays = calculateTotalDays(tasks); const startDate = new Date(2024, 0, 1); const dates = Array.from({ length: totalDays }, (_, i) => addDays(startDate, i)); const handleTaskDrag = (taskId, newStart, newEnd) => { setTasks(tasks.map(task => { if (task.id === taskId) { return { ...task, startDate: newStart, endDate: newEnd }; } return task; })); }; const addTask = () => { const newTask = { id: tasks.length + 1, name: "New Task", startDate: new Date(), endDate: addDays(new Date(), 7), progress: 0, color: getRandomColor(), // Utilisation de la couleur aléatoire days: 7, parentId: null, customFields: {} }; setTasks([...tasks, newTask]); setEditingTask(newTask); setEditMode(true); }; const deleteTask = (taskId) => { setTasks(tasks.filter(task => task.id !== taskId && task.parentId !== taskId)); }; const addCustomColumn = () => { if (!newColumn.name.trim()) { setError("Column name cannot be empty"); return; } setCustomColumns([...customColumns, newColumn]); setShowColumnModal(false); setNewColumn({ name: "", type: "text" }); }; const updateTask = (updatedTask) => { if (!updatedTask.name.trim()) { setError("Task name cannot be empty"); return; } if (updatedTask.startDate >= updatedTask.endDate) { setError("End date must be after start date"); return; } const days = differenceInDays(updatedTask.endDate, updatedTask.startDate); updatedTask.days = days; setTasks(tasks.map(task => task.id === updatedTask.id ? updatedTask : task )); setEditingTask(null); setError(""); }; const getTaskPosition = (task) => { const startDiff = differenceInDays(task.startDate, startDate); const duration = differenceInDays(task.endDate, task.startDate); const width = (duration / totalDays) * 100; const left = (startDiff / totalDays) * 100; return { width: `${width}%`, left: `${left}%` }; }; const getParentTasks = () => tasks.filter(task => !task.parentId); return ( <div className="p 4 bg white rounded lg shadow lg"> <div className="flex justify between mb 4"> <h2 className="text 2xl font bold text gray 800">Project Timeline</h2> <div className="space x 2"> <button onClick={() => setShowColumnModal(true)} className="px 4 py 2 text sm font medium text white bg purple 600 rounded md hover:bg purple 700 focus:outline none focus:ring 2 focus:ring purple 500" > Add Column </button> <button onClick={() => setEditMode(!editMode)} className="px 4 py 2 text sm font medium text white bg blue 600 rounded md hover:bg blue 700 focus:outline none focus:ring 2 focus:ring blue 500" > {editMode ? "View Mode" : "Edit Mode"} </button> {editMode && ( <button onClick={addTask} className="px 4 py 2 text sm font medium text white bg green 600 rounded md hover:bg green 700 focus:outline none focus:ring 2 focus:ring green 500" > <FiPlus className="inline mr 1" /> Add Task </button> )} </div> </div> {error && ( <div className="p 3 mb 4 text red 700 bg red 100 border border red 400 rounded"> {error} </div> )} <div className="overflow x auto"> <div className="min w max"> <div className="flex border b"> <div className="w 48 p 2 font semibold text gray 700">Task</div> <div className="w 32 p 2 font semibold text gray 700">Start Date</div> <div className="w 24 p 2 font semibold text gray 700">Days</div> <div className="w 24 p 2 font semibold text gray 700">Progress</div> <div className="w 32 p 2 font semibold text gray 700">Parent Task</div> {customColumns.map((column, index) => ( <div key={index} className="w 32 p 2 font semibold text gray 700"> {column.name} </div> ))} <div className="flex 1"> <div className="flex flex col"> <div className="flex"> {Array.from(new Set(dates.map(date => format(date, "MMMM yyyy")))).map((month, idx) => ( <div key={idx} className="text xs font semibold text center border l" style={{ width: `${dates.filter(date => format(date, "MMMM yyyy") === month).length * 32}px` }} > {month} </div> ))} </div> <div className="flex"> {dates.map((date, index) => ( <div key={index} className="w 8 p 2 text xs text center border l" > {format(date, "dd")} <div className="text xs text gray 500">{`W${getWeek(date)}`}</div> </div> ))} </div> </div> </div> </div> <div className="relative"> {tasks.map((task) => ( <div key={task.id} className="flex items center border b hover:bg gray 50"> <div className="w 48 p 2"> {editingTask?.id === task.id ? ( <input type="text" value={editingTask.name} onChange={(e) => setEditingTask({ ...editingTask, name: e.target.value })} className="w full p 1 border rounded" /> ) : ( <div className="flex items center justify between"> <span>{task.name}</span> {editMode && ( <div className="space x 1"> <button onClick={() => setEditingTask(task)} className="p 1 text blue 600 hover:text blue 800" > <FiEdit2 /> </button> <button onClick={() => deleteTask(task.id)} className="p 1 text red 600 hover:text red 800" > <FiTrash2 /> </button> </div> )} </div> )} </div> <div className="w 32 p 2 text sm text gray 600"> {format(task.startDate, "MMM d, yyyy")} </div> <div className="w 24 p 2 text sm text gray 600"> {task.days} days </div> <div className="w 24 p 2 text sm text gray 600"> {task.progress}% </div> <div className="w 32 p 2 text sm text gray 600"> {task.parentId ? tasks.find(t => t.id === task.parentId)?.name : " "} </div> {customColumns.map((column, index) => ( <div key={index} className="w 32 p 2 text sm text gray 600"> {editingTask?.id === task.id ? ( <input type={column.type} value={editingTask.customFields[column.name] || ""} onChange={(e) => setEditingTask({ ...editingTask, customFields: { ...editingTask.customFields, [column.name]: e.target.value } })} className="w full p 1 border rounded" /> ) : ( task.customFields[column.name] || " " )} </div> ))} <div className="flex 1 relative h 12"> <div className={`absolute h 8 mt 2 rounded ${task.color} opacity 75 cursor pointer transition all duration 200`} style={getTaskPosition(task)} draggable={editMode} onDragStart={(e) => { e.dataTransfer.setData("taskId", task.id.toString()); }} > <div className="h full bg white opacity 50" style={{ width: `${task.progress}%` }} /> </div> </div> </div> ))} </div> </div> </div> {editingTask && ( <div className="fixed inset 0 flex items center justify center bg black bg opacity 50"> <div className="p 6 bg white rounded lg shadow xl"> <h3 className="mb 4 text lg font semibold">Edit Task</h3> <div className="space y 4"> <div> <label className="block mb 1">Task Name</label> <input type="text" value={editingTask.name} onChange={(e) => setEditingTask({ ...editingTask, name: e.target.value })} className="w full p 2 border rounded" /> </div> <div> <label className="block mb 1">Parent Task</label> <select value={editingTask.parentId || ""} onChange={(e) => setEditingTask({ ...editingTask, parentId: e.target.value ? Number(e.target.value) : null })} className="w full p 2 border rounded" > <option value="">No Parent</option> {tasks .filter(t => t.id !== editingTask.id) .map(task => ( <option key={task.id} value={task.id}> {task.name} </option> ))} </select> </div> <div> <label className="block mb 1">Start Date</label> <input type="date" value={format(editingTask.startDate, "yyyy MM dd")} onChange={(e) => { const newStartDate = new Date(e.target.value); const newEndDate = addDays(newStartDate, editingTask.days); setEditingTask({ ...editingTask, startDate: newStartDate, endDate: newEndDate }); }} className="w full p 2 border rounded" /> </div> <div> <label className="block mb 1">Number of Days</label> <input type="number" min="1" value={editingTask.days} onChange={(e) => { const days = parseInt(e.target.value) || 1; const newEndDate = addDays(editingTask.startDate, days); setEditingTask({ ...editingTask, days: days, endDate: newEndDate }); }} className="w full p 2 border rounded" /> </div> <div> <label className="block mb 1">Progress (%)</label> <input type="number" min="0" max="100" value={editingTask.progress} onChange={(e) => setEditingTask({ ...editingTask, progress: Math.min(100, Math.max(0, parseInt(e.target.value) || 0)) })} className="w full p 2 border rounded" /> </div> {customColumns.map((column, index) => ( <div key={index}> <label className="block mb 1">{column.name}</label> <input type={column.type} value={editingTask.customFields[column.name] || ""} onChange={(e) => setEditingTask({ ...editingTask, customFields: { ...editingTask.customFields, [column.name]: e.target.value } })} className="w full p 2 border rounded" /> </div> ))} <div className="flex justify end space x 2"> <button onClick={() => updateTask(editingTask)} className="px 4 py 2 text white bg blue 600 rounded hover:bg blue 700" > <FiCheck className="inline mr 1" /> Save </button> <button onClick={() => { setEditingTask(null); setError(""); }} className="px 4 py 2 text gray 700 bg gray 200 rounded hover:bg gray 300" > <FiX className="inline mr 1" /> Cancel </button> </div> </div> </div> </div> )} {showColumnModal && ( <div className="fixed inset 0 flex items center justify center bg black bg opacity 50"> <div className="p 6 bg white rounded lg shadow xl"> <h3 className="mb 4 text lg font semibold">Add Custom Column</h3> <div className="space y 4"> <div> <label className="block mb 1">Column Name</label> <input type="text" value={newColumn.name} onChange={(e) => setNewColumn({ ...newColumn, name: e.target.value })} className="w full p 2 border rounded" /> </div> <div> <label className="block mb 1">Column Type</label> <select value={newColumn.type} onChange={(e) => setNewColumn({ ...newColumn, type: e.target.value })} className="w full p 2 border rounded" > <option value="text">Text</option> <option value="number">Number</option> <option value="date">Date</option> <option value="email">Email</option> </select> </div> <div className="flex justify end space x 2"> <button onClick={addCustomColumn} className="px 4 py 2 text white bg blue 600 rounded hover:bg blue 700" > <FiCheck className="inline mr 1" /> Add </button> <button onClick={() => { setShowColumnModal(false); setNewColumn({ name: "", type: "text" }); }} className="px 4 py 2 text gray 700 bg gray 200 rounded hover:bg gray 300" > <FiX className="inline mr 1" /> Cancel </button> </div> </div> </div> </div> )} </div> ); }; export default GanttChart;

Prompt
Component Preview

About

GanttChart - Visualize project timelines with parent-child task grouping, drag-and-drop support, and custom columns, built with React. Copy component code!

Share

Last updated 1 month ago