This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user