255 lines
7.8 KiB
TypeScript
255 lines
7.8 KiB
TypeScript
import { Add, AddCircle, CloseCircle } from "iconsax-react";
|
||
import { useRef, useState, type FC } from "react";
|
||
import { useTranslation } from "react-i18next";
|
||
import { toast } from "react-toastify";
|
||
import ModalConfrim from "../../../components/ModalConfrim";
|
||
import { ErrorType } from "../../../helpers/types";
|
||
import { useDeleteTaskPhase } from "../task-phase/hooks/useTaskPhaseData";
|
||
import type { Column as ColumnType, Task as TaskType } from "../types";
|
||
import ColumnMenu from "./ColumnMenu";
|
||
import Task from "./Task";
|
||
|
||
type Props = {
|
||
column: ColumnType;
|
||
projectId: string;
|
||
tasks: TaskType[];
|
||
draggedTaskId: string | null;
|
||
draggedColumnId: string | null;
|
||
onDragStart: (taskId: string) => void;
|
||
onDragEnd: () => void;
|
||
onDrop: (columnId: string, index: number) => void;
|
||
onColumnDragStart: (columnId: string) => void;
|
||
onColumnDragEnd: () => void;
|
||
onColumnDrop: (overColumnId: string) => void;
|
||
onTaskClick?: (task: TaskType) => void;
|
||
onColumnDeleted?: (columnId: string) => void;
|
||
};
|
||
|
||
const Column: FC<Props> = ({
|
||
column,
|
||
projectId,
|
||
tasks,
|
||
draggedTaskId,
|
||
draggedColumnId,
|
||
onDragStart,
|
||
onDragEnd,
|
||
onDrop,
|
||
onColumnDragStart,
|
||
onColumnDragEnd,
|
||
onColumnDrop,
|
||
onTaskClick,
|
||
onColumnDeleted,
|
||
}) => {
|
||
const { t } = useTranslation("global");
|
||
const deleteTaskPhase = useDeleteTaskPhase();
|
||
|
||
const [isAdding, setIsAdding] = useState(false);
|
||
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
|
||
const [isDragOver, setIsDragOver] = useState(false);
|
||
const [isColumnDragOver, setIsColumnDragOver] = useState(false);
|
||
const columnRef = useRef<HTMLDivElement>(null);
|
||
const dragImageRef = useRef<HTMLElement | null>(null);
|
||
const isDraggingColumn = draggedColumnId === column.id;
|
||
|
||
const handleColumnDragStart = (e: React.DragEvent) => {
|
||
const columnEl = columnRef.current;
|
||
if (columnEl) {
|
||
const clone = columnEl.cloneNode(true) as HTMLElement;
|
||
clone.style.position = "fixed";
|
||
clone.style.top = "-10000px";
|
||
clone.style.left = "-10000px";
|
||
clone.style.width = `${columnEl.offsetWidth}px`;
|
||
clone.style.opacity = "0.95";
|
||
clone.style.boxShadow = "0 8px 24px rgba(0, 0, 0, 0.15)";
|
||
clone.style.pointerEvents = "none";
|
||
clone.style.zIndex = "9999";
|
||
document.body.appendChild(clone);
|
||
dragImageRef.current = clone;
|
||
|
||
const rect = columnEl.getBoundingClientRect();
|
||
e.dataTransfer.setDragImage(
|
||
clone,
|
||
e.clientX - rect.left,
|
||
e.clientY - rect.top,
|
||
);
|
||
e.dataTransfer.effectAllowed = "move";
|
||
}
|
||
|
||
onColumnDragStart(column.id);
|
||
};
|
||
|
||
const handleColumnDragEnd = () => {
|
||
if (dragImageRef.current) {
|
||
document.body.removeChild(dragImageRef.current);
|
||
dragImageRef.current = null;
|
||
}
|
||
onColumnDragEnd();
|
||
};
|
||
|
||
const handleDragOver = (e: React.DragEvent) => {
|
||
if (draggedColumnId) return;
|
||
e.preventDefault();
|
||
e.dataTransfer.dropEffect = "move";
|
||
setIsDragOver(true);
|
||
};
|
||
|
||
const handleDragLeave = (e: React.DragEvent) => {
|
||
if (draggedColumnId) return;
|
||
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
||
setIsDragOver(false);
|
||
}
|
||
};
|
||
|
||
const handleDrop = (e: React.DragEvent, index: number) => {
|
||
if (draggedColumnId) return;
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
setIsDragOver(false);
|
||
onDrop(column.id, index);
|
||
};
|
||
|
||
const handleColumnDragOver = (e: React.DragEvent) => {
|
||
if (!draggedColumnId || draggedColumnId === column.id) return;
|
||
e.preventDefault();
|
||
e.dataTransfer.dropEffect = "move";
|
||
setIsColumnDragOver(true);
|
||
};
|
||
|
||
const handleColumnDragLeave = (e: React.DragEvent) => {
|
||
if (!draggedColumnId) return;
|
||
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
||
setIsColumnDragOver(false);
|
||
}
|
||
};
|
||
|
||
const handleColumnDrop = (e: React.DragEvent) => {
|
||
if (!draggedColumnId || draggedColumnId === column.id) return;
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
setIsColumnDragOver(false);
|
||
onColumnDrop(column.id);
|
||
};
|
||
|
||
const handleDeleteColumn = () => {
|
||
deleteTaskPhase.mutate(
|
||
{ id: column.id, projectId },
|
||
{
|
||
onSuccess: () => {
|
||
onColumnDeleted?.(column.id);
|
||
setIsDeleteConfirmOpen(false);
|
||
toast.success(t("taskmanager.column_deleted"));
|
||
},
|
||
onError: (error: ErrorType) => {
|
||
toast.error(error.response?.data?.error.message[0]);
|
||
},
|
||
},
|
||
);
|
||
};
|
||
|
||
return (
|
||
<div
|
||
ref={columnRef}
|
||
className={`bg-[#F0F3F7] rounded-xl p-3 sm:p-4 xl:p-6 w-[272px] sm:w-[288px] xl:w-[310px] shrink-0 flex flex-col max-h-[calc(100dvh-140px)] sm:max-h-[calc(100dvh-152px)] xl:max-h-[calc(100dvh-220px)] transition-colors ${
|
||
isDragOver ? "ring-2 ring-primary/40" : ""
|
||
} ${isColumnDragOver ? "ring-2 ring-black/30" : ""} ${
|
||
isDraggingColumn ? "opacity-50" : ""
|
||
}`}
|
||
onDragOver={(e) => {
|
||
handleColumnDragOver(e);
|
||
handleDragOver(e);
|
||
}}
|
||
onDragLeave={(e) => {
|
||
handleColumnDragLeave(e);
|
||
handleDragLeave(e);
|
||
}}
|
||
onDrop={(e) => {
|
||
handleColumnDrop(e);
|
||
if (!draggedColumnId) {
|
||
handleDrop(e, tasks.length);
|
||
}
|
||
}}
|
||
>
|
||
<div className="flex justify-between items-center shrink-0 gap-2">
|
||
<div
|
||
draggable
|
||
onDragStart={handleColumnDragStart}
|
||
onDragEnd={handleColumnDragEnd}
|
||
className="flex-1 min-w-0 font-bold text-sm truncate cursor-grab active:cursor-grabbing"
|
||
>
|
||
{column.title}
|
||
</div>
|
||
<ColumnMenu onDelete={() => setIsDeleteConfirmOpen(true)} />
|
||
</div>
|
||
|
||
<ModalConfrim
|
||
isOpen={isDeleteConfirmOpen}
|
||
close={() => setIsDeleteConfirmOpen(false)}
|
||
onConfrim={handleDeleteColumn}
|
||
isLoading={deleteTaskPhase.isPending}
|
||
label={t("taskmanager.delete_column_confirm")}
|
||
/>
|
||
|
||
<div className="mt-3 sm:mt-4 xl:mt-5 flex-1 min-h-0 overflow-y-auto overscroll-y-contain flex flex-col gap-3 sm:gap-4">
|
||
{tasks.map((task, index) => (
|
||
<div
|
||
key={task.id}
|
||
onDragOver={(e) => {
|
||
if (draggedColumnId) return;
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
}}
|
||
onDrop={(e) => handleDrop(e, index)}
|
||
>
|
||
<Task
|
||
task={task}
|
||
isDragging={draggedTaskId === task.id}
|
||
onDragStart={onDragStart}
|
||
onDragEnd={onDragEnd}
|
||
onClick={onTaskClick}
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{isAdding ? (
|
||
<div className="mt-3 sm:mt-4 xl:mt-5 shrink-0 flex flex-col gap-3">
|
||
<input
|
||
type="text"
|
||
placeholder="عنوان تسک"
|
||
className="w-full bg-white rounded-lg px-3 py-2.5 text-sm outline-none"
|
||
autoFocus
|
||
/>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
type="button"
|
||
className="flex items-center gap-2 bg-black text-white text-[13px] rounded-full px-4 py-2 cursor-pointer"
|
||
>
|
||
<span>اضافه کردن</span>
|
||
<Add size={18} color="white" />
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setIsAdding(false)}
|
||
className="cursor-pointer"
|
||
aria-label="انصراف"
|
||
>
|
||
<CloseCircle size={22} color="#888888" variant="Bold" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<button
|
||
type="button"
|
||
onClick={() => setIsAdding(true)}
|
||
className="mt-3 sm:mt-4 xl:mt-5 flex gap-2 items-center shrink-0 cursor-pointer"
|
||
>
|
||
<AddCircle size={18} color="#888888" />
|
||
<span className="text-description text-[13px] mt-0.5">اضافه کردن تسک</span>
|
||
</button>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default Column;
|