From 328402fd7f35b8a89b4fa6d7ac57b49770af7f6e Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Thu, 11 Jun 2026 12:30:19 +0330 Subject: [PATCH] drag and drop --- package-lock.json | 150 ++++++++++++-------- package.json | 1 + src/pages/taskmanager/components/Column.tsx | 75 ++++++++-- src/pages/taskmanager/components/Task.tsx | 33 ++++- src/pages/taskmanager/data/tasks.json | 53 +++++++ src/pages/taskmanager/types.ts | 21 +++ src/pages/taskmanager/utils/reorderTasks.ts | 38 +++++ src/pages/taskmanager/workspace.tsx | 58 +++++--- tsconfig.app.json | 1 + 9 files changed, 339 insertions(+), 91 deletions(-) create mode 100644 src/pages/taskmanager/data/tasks.json create mode 100644 src/pages/taskmanager/types.ts create mode 100644 src/pages/taskmanager/utils/reorderTasks.ts diff --git a/package-lock.json b/package-lock.json index eb8e038..7334407 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@headlessui/react": "^2.2.0", + "@hello-pangea/dnd": "^18.0.1", "@material-tailwind/react": "^2.1.10", "@tanstack/react-query": "^5.62.8", "axios": "^1.7.9", @@ -1568,7 +1569,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1585,7 +1585,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1602,7 +1601,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1619,7 +1617,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1636,7 +1633,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1653,7 +1649,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1670,7 +1665,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1687,7 +1681,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1704,7 +1697,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1721,7 +1713,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1738,7 +1729,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1755,7 +1745,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1772,7 +1761,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1789,7 +1777,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1806,7 +1793,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1823,7 +1809,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1840,7 +1825,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1857,7 +1841,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1874,7 +1857,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1891,7 +1873,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1908,7 +1889,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1925,7 +1905,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1942,7 +1921,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1959,7 +1937,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1976,7 +1953,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2215,6 +2191,23 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/@hello-pangea/dnd": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz", + "integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.26.7", + "css-box-model": "^1.2.1", + "raf-schd": "^4.0.3", + "react-redux": "^9.2.0", + "redux": "^5.0.1" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2747,7 +2740,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2761,7 +2753,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2775,7 +2766,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2789,7 +2779,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2803,7 +2792,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2817,7 +2805,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2831,7 +2818,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2845,7 +2831,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2859,7 +2844,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2873,7 +2857,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2887,7 +2870,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2901,7 +2883,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2915,7 +2896,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2929,7 +2909,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2943,7 +2922,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2957,7 +2935,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2971,7 +2948,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2985,7 +2961,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2999,7 +2974,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3013,7 +2987,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3098,7 +3071,7 @@ "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", @@ -3112,7 +3085,7 @@ "version": "7.6.8", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" @@ -3122,7 +3095,7 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", @@ -3133,7 +3106,7 @@ "version": "7.20.6", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/types": "^7.20.7" @@ -3235,6 +3208,12 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.1.tgz", @@ -4136,6 +4115,15 @@ "node": ">=8" } }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "license": "MIT", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -4491,7 +4479,6 @@ "version": "0.25.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -6050,7 +6037,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -6434,7 +6421,6 @@ "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "dev": true, "funding": [ { "type": "github", @@ -6786,7 +6772,6 @@ "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "dev": true, "funding": [ { "type": "opencollective", @@ -7045,6 +7030,12 @@ "node": ">= 12.0.0" } }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", + "license": "MIT" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -7226,6 +7217,29 @@ "react-dom": ">=16.8.6 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -7320,6 +7334,12 @@ "node": ">=8.10.0" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -7496,7 +7516,6 @@ "version": "4.40.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.7" @@ -7840,7 +7859,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -8352,6 +8370,12 @@ "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", "license": "MIT" }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -8557,7 +8581,7 @@ "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -8745,6 +8769,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -8756,7 +8789,6 @@ "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", - "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -8861,7 +8893,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -8879,7 +8910,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -9477,7 +9507,7 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 4c569b7..6cc08df 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@headlessui/react": "^2.2.0", + "@hello-pangea/dnd": "^18.0.1", "@material-tailwind/react": "^2.1.10", "@tanstack/react-query": "^5.62.8", "axios": "^1.7.9", diff --git a/src/pages/taskmanager/components/Column.tsx b/src/pages/taskmanager/components/Column.tsx index 8810621..2c84c20 100644 --- a/src/pages/taskmanager/components/Column.tsx +++ b/src/pages/taskmanager/components/Column.tsx @@ -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 = ({ title, children }) => { +const Column: FC = ({ + 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 ( -
+
handleDrop(e, tasks.length)} + >
-
{title}
+
{column.title}
-
{children}
+ {tasks.length > 0 && ( +
+ {tasks.map((task, index) => ( +
{ + e.preventDefault(); + e.stopPropagation(); + }} + onDrop={(e) => handleDrop(e, index)} + > + +
+ ))} +
+ )} {isAdding ? (
diff --git a/src/pages/taskmanager/components/Task.tsx b/src/pages/taskmanager/components/Task.tsx index 2d819e8..a27a72c 100644 --- a/src/pages/taskmanager/components/Task.tsx +++ b/src/pages/taskmanager/components/Task.tsx @@ -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 = ({ task, isDragging, onDragStart, onDragEnd }) => { return ( -
+
onDragStart(task.id)} + onDragEnd={onDragEnd} + className={`bg-white rounded-lg p-2 cursor-grab active:cursor-grabbing transition-opacity ${ + isDragging ? "opacity-40" : "" + }`} + >
-
برچشب
-
اضافه شدن پیش نمایش
+
+ {task.tag} +
+
{task.title}
-
۱ اردیبهشت - ۲۰ اردیبهشت
+
{task.dateRange}
-
1
+
{task.attachments}
-
1/5
+
+ {task.checklistDone}/{task.checklistTotal} +
diff --git a/src/pages/taskmanager/data/tasks.json b/src/pages/taskmanager/data/tasks.json new file mode 100644 index 0000000..f5e65a7 --- /dev/null +++ b/src/pages/taskmanager/data/tasks.json @@ -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 + } + ] +} diff --git a/src/pages/taskmanager/types.ts b/src/pages/taskmanager/types.ts new file mode 100644 index 0000000..477d3c8 --- /dev/null +++ b/src/pages/taskmanager/types.ts @@ -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[]; +}; diff --git a/src/pages/taskmanager/utils/reorderTasks.ts b/src/pages/taskmanager/utils/reorderTasks.ts new file mode 100644 index 0000000..8bfccfa --- /dev/null +++ b/src/pages/taskmanager/utils/reorderTasks.ts @@ -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]; +}; diff --git a/src/pages/taskmanager/workspace.tsx b/src/pages/taskmanager/workspace.tsx index 114b4f6..bc2c967 100644 --- a/src/pages/taskmanager/workspace.tsx +++ b/src/pages/taskmanager/workspace.tsx @@ -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(tasksData.tasks as Task[]); + const [draggedTaskId, setDraggedTaskId] = useState(null); + + const tasksByColumn = useMemo(() => { + return columns.reduce>((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 (
- -
- - - -
-
- - - -
- -
-
+ {columns.map((column) => ( + + ))}
diff --git a/tsconfig.app.json b/tsconfig.app.json index 358ca9b..fe50a1c 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -12,6 +12,7 @@ "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", + "resolveJsonModule": true, "noEmit": true, "jsx": "react-jsx",