Gantt Chart - Copy this React, Tailwind Component to your project
Add-the-functionality-to-have-parents-and-children-to-this-code-(for-exemple-the-tasks-Design-Phase-and-Development-could-be-in-the-group-of-Project-Planning-the-parent)-and-remove-column-Parent-Task)-here-is-the-code:-"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;
