drag and drop
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
hamid zarghami
2026-06-11 12:30:19 +03:30
parent eb2f23c403
commit 328402fd7f
9 changed files with 339 additions and 91 deletions
+67 -8
View File
@@ -1,23 +1,82 @@
import { Add, AddCircle, CloseCircle, More } from "iconsax-react";
import { Children, useState, type FC, type ReactNode } from "react";
import { useState, type FC } from "react";
import type { Column as ColumnType, Task as TaskType } from "../types";
import Task from "./Task";
type Props = {
title: string;
children?: ReactNode;
column: ColumnType;
tasks: TaskType[];
draggedTaskId: string | null;
onDragStart: (taskId: string) => void;
onDragEnd: () => void;
onDrop: (columnId: string, index: number) => void;
};
const Column: FC<Props> = ({ title, children }) => {
const Column: FC<Props> = ({
column,
tasks,
draggedTaskId,
onDragStart,
onDragEnd,
onDrop,
}) => {
const [isAdding, setIsAdding] = useState(false);
const hasTasks = Children.count(children) > 0;
const [isDragOver, setIsDragOver] = useState(false);
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
setIsDragOver(true);
};
const handleDragLeave = (e: React.DragEvent) => {
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setIsDragOver(false);
}
};
const handleDrop = (e: React.DragEvent, index: number) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
onDrop(column.id, index);
};
return (
<div className="bg-[#F0F3F7] rounded-xl p-6 max-w-[310px] w-full flex flex-col self-start">
<div
className={`bg-[#F0F3F7] rounded-xl p-6 max-w-[310px] w-full flex flex-col self-start transition-colors ${
isDragOver ? "ring-2 ring-primary/40" : ""
}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, tasks.length)}
>
<div className="flex justify-between items-center shrink-0">
<div className="font-bold text-sm">{title}</div>
<div className="font-bold text-sm">{column.title}</div>
<More size={20} color="black" />
</div>
<div className={hasTasks ? "mt-5" : ""}>{children}</div>
{tasks.length > 0 && (
<div className="mt-5 flex flex-col gap-4">
{tasks.map((task, index) => (
<div
key={task.id}
onDragOver={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onDrop={(e) => handleDrop(e, index)}
>
<Task
task={task}
isDragging={draggedTaskId === task.id}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
/>
</div>
))}
</div>
)}
{isAdding ? (
<div className="mt-5 shrink-0 flex flex-col gap-3">
+26 -7
View File
@@ -1,25 +1,44 @@
import { AttachCircle, Calendar, TickSquare } from "iconsax-react";
import { type FC } from "react";
import type { Task as TaskType } from "../types";
const Task: FC = () => {
type Props = {
task: TaskType;
isDragging?: boolean;
onDragStart: (taskId: string) => void;
onDragEnd: () => void;
};
const Task: FC<Props> = ({ task, isDragging, onDragStart, onDragEnd }) => {
return (
<div className="bg-white rounded-lg p-2">
<div
draggable
onDragStart={() => onDragStart(task.id)}
onDragEnd={onDragEnd}
className={`bg-white rounded-lg p-2 cursor-grab active:cursor-grabbing transition-opacity ${
isDragging ? "opacity-40" : ""
}`}
>
<div className="h-10 bg-yellow-500 rounded-lg"></div>
<div className="bg-[#FF76C2] h-5 px-3 flex items-center mt-2 text-[10px] text-white rounded-md w-fit">برچشب</div>
<div className="font-bold text-xs mt-2">اضافه شدن پیش نمایش</div>
<div className="bg-[#FF76C2] h-5 px-3 flex items-center mt-2 text-[10px] text-white rounded-md w-fit">
{task.tag}
</div>
<div className="font-bold text-xs mt-2">{task.title}</div>
<div className="mt-2 flex items-center gap-4 text-xs">
<div className="flex items-center gap-1 text-[10px]">
<Calendar size={16} color="#292D32" />
<div>۱ اردیبهشت - ۲۰ اردیبهشت</div>
<div>{task.dateRange}</div>
</div>
<div className="flex gap-1">
<AttachCircle size={16} color="#292D32" />
<div>1</div>
<div>{task.attachments}</div>
</div>
<div className="flex gap-1">
<TickSquare size={16} color="#292D32" />
<div>1/5</div>
<div>
{task.checklistDone}/{task.checklistTotal}
</div>
</div>
</div>
+53
View File
@@ -0,0 +1,53 @@
{
"columns": [
{ "id": "todo", "title": "برای انجام" },
{ "id": "in-progress", "title": "درحال انجام" },
{ "id": "done", "title": "انجام شده" }
],
"tasks": [
{
"id": "task-1",
"columnId": "todo",
"order": 0,
"title": "اضافه شدن پیش نمایش",
"tag": "برچشب",
"dateRange": "۱ اردیبهشت - ۲۰ اردیبهشت",
"attachments": 1,
"checklistDone": 1,
"checklistTotal": 5
},
{
"id": "task-2",
"columnId": "todo",
"order": 1,
"title": "طراحی صفحه اصلی",
"tag": "طراحی",
"dateRange": "۵ اردیبهشت - ۱۵ اردیبهشت",
"attachments": 2,
"checklistDone": 3,
"checklistTotal": 8
},
{
"id": "task-3",
"columnId": "todo",
"order": 2,
"title": "بررسی فیدبک کاربران",
"tag": "بازبینی",
"dateRange": "۱۰ اردیبهشت - ۱۲ اردیبهشت",
"attachments": 0,
"checklistDone": 0,
"checklistTotal": 3
},
{
"id": "task-4",
"columnId": "done",
"order": 0,
"title": "تنظیمات اولیه پروژه",
"tag": "راه‌اندازی",
"dateRange": "۱ فروردین - ۵ فروردین",
"attachments": 1,
"checklistDone": 5,
"checklistTotal": 5
}
]
}
+21
View File
@@ -0,0 +1,21 @@
export type Column = {
id: string;
title: string;
};
export type Task = {
id: string;
columnId: string;
order: number;
title: string;
tag: string;
dateRange: string;
attachments: number;
checklistDone: number;
checklistTotal: number;
};
export type WorkspaceData = {
columns: Column[];
tasks: Task[];
};
@@ -0,0 +1,38 @@
import type { Task } from "../types";
export const reorderTasks = (
tasks: Task[],
taskId: string,
destColumnId: string,
destIndex: number,
): Task[] => {
const task = tasks.find((t) => t.id === taskId);
if (!task) return tasks;
const sourceColumnId = task.columnId;
const without = tasks.filter((t) => t.id !== taskId);
const destColumnTasks = without
.filter((t) => t.columnId === destColumnId)
.sort((a, b) => a.order - b.order);
destColumnTasks.splice(destIndex, 0, { ...task, columnId: destColumnId });
const updatedDest = destColumnTasks.map((t, i) => ({ ...t, order: i }));
if (sourceColumnId === destColumnId) {
const rest = without.filter((t) => t.columnId !== destColumnId);
return [...rest, ...updatedDest];
}
const sourceTasks = without
.filter((t) => t.columnId === sourceColumnId)
.sort((a, b) => a.order - b.order)
.map((t, i) => ({ ...t, order: i }));
const rest = without.filter(
(t) => t.columnId !== destColumnId && t.columnId !== sourceColumnId,
);
return [...rest, ...sourceTasks, ...updatedDest];
};
+42 -16
View File
@@ -1,28 +1,54 @@
import { type FC } from "react";
import { useMemo, useState, type FC } from "react";
import AddNewColumn from "./components/AddNewColumn";
import Column from "./components/Column";
import HeaderWorkspace from "./components/HeaderWorkspace";
import Task from "./components/Task";
import tasksData from "./data/tasks.json";
import type { Task, WorkspaceData } from "./types";
import { reorderTasks } from "./utils/reorderTasks";
const Workspace: FC = () => {
const { columns } = tasksData as WorkspaceData;
const [tasks, setTasks] = useState<Task[]>(tasksData.tasks as Task[]);
const [draggedTaskId, setDraggedTaskId] = useState<string | null>(null);
const tasksByColumn = useMemo(() => {
return columns.reduce<Record<string, Task[]>>((acc, column) => {
acc[column.id] = tasks
.filter((task) => task.columnId === column.id)
.sort((a, b) => a.order - b.order);
return acc;
}, {});
}, [columns, tasks]);
const handleDragStart = (taskId: string) => {
setDraggedTaskId(taskId);
};
const handleDragEnd = () => {
setDraggedTaskId(null);
};
const handleDrop = (columnId: string, index: number) => {
if (!draggedTaskId) return;
setTasks((prev) => reorderTasks(prev, draggedTaskId, columnId, index));
setDraggedTaskId(null);
};
return (
<div className="bg-[#01347A] h-full rounded-[32px] overflow-hidden flex flex-col">
<HeaderWorkspace />
<div className="px-8 flex gap-7 mt-6 flex-1 min-h-0 overflow-auto pb-6 items-start">
<Column title="برای انجام">
<div className="flex flex-col gap-4">
<Task />
<Task />
<Task />
</div>
</Column>
<Column title="درحال انجام" />
<Column title="انجام شده">
<div className="flex flex-col gap-4">
<Task />
</div>
</Column>
{columns.map((column) => (
<Column
key={column.id}
column={column}
tasks={tasksByColumn[column.id] ?? []}
draggedTaskId={draggedTaskId}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDrop={handleDrop}
/>
))}
<AddNewColumn />
</div>