diff --git a/bun.lock b/bun.lock
index 070c038..60ce50d 100644
--- a/bun.lock
+++ b/bun.lock
@@ -36,6 +36,9 @@
"frontend": {
"name": "@trmp/frontend",
"dependencies": {
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
@@ -71,7 +74,9 @@
"react-day-picker": "^9.11.2",
"react-dom": "^19.2.0",
"react-hook-form": "^7.66.1",
+ "react-markdown": "^10.1.0",
"react-router-dom": "^7.9.6",
+ "remark-gfm": "^4.0.1",
"shadcn": "^3.5.0",
"tailwind-merge": "^3.4.0",
"zod": "^4.1.13",
@@ -162,6 +167,14 @@
"@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
+ "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="],
+
+ "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="],
+
+ "@dnd-kit/sortable": ["@dnd-kit/sortable@10.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg=="],
+
+ "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="],
+
"@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.51.1", "", { "dependencies": { "commander": "^11.1.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.2", "which": "^4.0.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-fqcQxcxC4LOaUlW8IkyWw8x0yirlLUkbxohz9OnWvVWjf73J5yyw7jxWnkOJaUKXZotcGEScDox9MU6rSkcDgg=="],
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
@@ -566,12 +579,22 @@
"@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
+ "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
+
"@types/eslint-plugin-jsx-a11y": ["@types/eslint-plugin-jsx-a11y@6.10.1", "", { "dependencies": { "eslint": "^9" } }, "sha512-5RtuPVe0xz8BAhrkn2oww6Uw885atf962Q4fqZo48QdO3EQA7oCEDSXa6optgJ1ZMds3HD9ITK5bfm4AWuoXFQ=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+ "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
+
+ "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
+
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
+ "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
+
+ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
+
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
"@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="],
@@ -582,6 +605,8 @@
"@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="],
+ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
+
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.48.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/type-utils": "8.48.0", "@typescript-eslint/utils": "8.48.0", "@typescript-eslint/visitor-keys": "8.48.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.48.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.48.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/types": "8.48.0", "@typescript-eslint/typescript-estree": "8.48.0", "@typescript-eslint/visitor-keys": "8.48.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ=="],
@@ -602,6 +627,8 @@
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.48.0", "", { "dependencies": { "@typescript-eslint/types": "8.48.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg=="],
+ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
+
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.1", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.47", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
@@ -660,6 +687,8 @@
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
+ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
+
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.31", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw=="],
@@ -690,8 +719,18 @@
"caniuse-lite": ["caniuse-lite@1.0.30001756", "", {}, "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A=="],
+ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
+
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
+ "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
+
+ "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
+
+ "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
+
+ "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
+
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
@@ -714,6 +753,8 @@
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
+ "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
+
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
@@ -760,6 +801,8 @@
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+ "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
+
"dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
@@ -774,10 +817,14 @@
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
+ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
+
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
@@ -870,6 +917,8 @@
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
+ "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
+
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
@@ -884,6 +933,8 @@
"express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="],
+ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
+
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
@@ -990,12 +1041,18 @@
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+ "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
+
+ "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
+
"headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="],
"hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
+ "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
+
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
@@ -1012,10 +1069,16 @@
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
+ "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
+
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
+ "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
+
+ "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
+
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
@@ -1034,6 +1097,8 @@
"is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
+ "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
+
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
@@ -1044,6 +1109,8 @@
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+ "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
+
"is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
"is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
@@ -1156,6 +1223,8 @@
"log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="],
+ "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
+
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
@@ -1164,8 +1233,40 @@
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+ "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
+
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+ "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
+
+ "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
+
+ "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
+
+ "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
+
+ "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
+
+ "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
+
+ "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
+
+ "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
+
+ "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
+
+ "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
+
+ "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="],
+
+ "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
+
+ "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
+
+ "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
+
+ "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
+
"mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
@@ -1176,6 +1277,62 @@
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
+ "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
+
+ "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
+
+ "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
+
+ "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
+
+ "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
+
+ "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
+
+ "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
+
+ "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
+
+ "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
+
+ "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
+
+ "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
+
+ "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
+
+ "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
+
+ "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
+
+ "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
+
+ "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
+
+ "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
+
+ "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
+
+ "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
+
+ "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
+
+ "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
+
+ "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
+
+ "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
+
+ "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
+
+ "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
+
+ "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
+
+ "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
+
+ "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
+
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
@@ -1250,6 +1407,8 @@
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
+ "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
+
"parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
"parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
@@ -1310,6 +1469,8 @@
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
+ "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
+
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
@@ -1334,6 +1495,8 @@
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
+ "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
+
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
"react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="],
@@ -1352,6 +1515,14 @@
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
+ "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
+
+ "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
+
+ "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
+
+ "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
+
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
@@ -1428,6 +1599,8 @@
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
+ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
+
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
@@ -1452,6 +1625,8 @@
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
+ "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
+
"stringify-object": ["stringify-object@5.0.0", "", { "dependencies": { "get-own-enumerable-keys": "^1.0.0", "is-obj": "^3.0.0", "is-regexp": "^3.1.0" } }, "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg=="],
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
@@ -1462,6 +1637,10 @@
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
+ "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
+
+ "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
+
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
@@ -1492,6 +1671,10 @@
"tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="],
+ "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
+
+ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
+
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
"ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="],
@@ -1528,6 +1711,18 @@
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
+ "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
+
+ "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
+
+ "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
+
+ "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
+
+ "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
+
+ "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
+
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
@@ -1546,6 +1741,10 @@
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
+ "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
+
+ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
+
"vite": ["vite@7.2.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w=="],
"vite-plugin-image-optimizer": ["vite-plugin-image-optimizer@2.0.3", "", { "dependencies": { "ansi-colors": "^4.1.3", "pathe": "^2.0.3" }, "peerDependencies": { "sharp": ">=0.34.0", "svgo": ">=4", "vite": ">=5" }, "optionalPeers": ["sharp", "svgo"] }, "sha512-1vrFOTcpSvv6DCY7h8UXab4wqMAjTJB/ndOzG/Kmj1oDOuPF6mbjkNQoGzzCEYeWGe7qU93jc8oQqvoJ57al3A=="],
@@ -1590,6 +1789,8 @@
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
+ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
+
"@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
"@dotenvx/dotenvx/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
@@ -1694,12 +1895,16 @@
"log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
+ "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
+
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
"ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
+ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
+
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
diff --git a/frontend/package.json b/frontend/package.json
index 6b43d69..656d5bf 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -8,6 +8,9 @@
"preview": "vite preview"
},
"dependencies": {
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
@@ -43,7 +46,9 @@
"react-day-picker": "^9.11.2",
"react-dom": "^19.2.0",
"react-hook-form": "^7.66.1",
+ "react-markdown": "^10.1.0",
"react-router-dom": "^7.9.6",
+ "remark-gfm": "^4.0.1",
"shadcn": "^3.5.0",
"tailwind-merge": "^3.4.0",
"zod": "^4.1.13"
diff --git a/frontend/src/components/ui/task-components/AssigneesCombobox.tsx b/frontend/src/components/ui/task-components/AssigneesCombobox.tsx
new file mode 100644
index 0000000..4ad7cd6
--- /dev/null
+++ b/frontend/src/components/ui/task-components/AssigneesCombobox.tsx
@@ -0,0 +1,118 @@
+// Allows users to select team members as assignees for a task
+
+import { Check, ChevronsUpDown, X } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+} from "@/components/ui/command";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { Badge } from "@/components/ui/badge";
+import { Avatar, AvatarFallback } from "@/components/ui/avatar";
+import { useState } from "react";
+import { teamMembers } from "@/lib/dummyData/dummyTasks";
+
+interface Props {
+ selectedAssignees: string[];
+ onAssigneesChange: (assignees: string[]) => void;
+}
+
+export function AssigneesCombobox({ selectedAssignees, onAssigneesChange }: Props) {
+ const [open, setOpen] = useState(false);
+
+ const toggleAssignee = (assigneeValue: string) => {
+ if (selectedAssignees.includes(assigneeValue)) {
+ onAssigneesChange(selectedAssignees.filter((assignee) => assignee !== assigneeValue));
+ } else {
+ onAssigneesChange([...selectedAssignees, assigneeValue]);
+ }
+ };
+
+ const removeAssignee = (assigneeValue: string) => {
+ onAssigneesChange(selectedAssignees.filter((assignee) => assignee !== assigneeValue));
+ };
+
+ return (
+ // container for both the flex-wrap and assignee popover
+
+
+
+
+
+
+
+
+ No team members found.
+
+ {teamMembers.map((member) => (
+ toggleAssignee(member.value)}
+ className="cursor-pointer"
+ >
+
+
+
+
+ {member.initials}
+
+
+ {member.label}
+
+
+ ))}
+
+
+
+
+ {/* displays the different assignee badges for selection display */}
+ {selectedAssignees.length > 0 && (
+
+ {selectedAssignees.map((assigneeValue) => {
+ const member = teamMembers.find((member) => member.value === assigneeValue);
+ if (!member) return null;
+ return (
+
+
+
+ {member.initials}
+
+
+ {member.label}
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
diff --git a/frontend/src/components/ui/task-components/ColumnContainer.tsx b/frontend/src/components/ui/task-components/ColumnContainer.tsx
new file mode 100644
index 0000000..61b7285
--- /dev/null
+++ b/frontend/src/components/ui/task-components/ColumnContainer.tsx
@@ -0,0 +1,499 @@
+// Component for an entire Kanban column
+// Renders Column Header and Title
+// Renders the tasks inside the columns
+// Handles task creation and editing inside each column
+// Dropdown for moving columns left and right
+// Allows for deleting and editing column creation
+
+import { Card } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { useMemo, useState, useEffect, useCallback } from "react";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+ Plus,
+ Ellipsis,
+ Trash,
+ Circle,
+ CalendarIcon,
+ Pencil,
+ PaperclipIcon,
+ MoveLeftIcon,
+ MoveRightIcon,
+} from "lucide-react";
+import { SortableContext } from "@dnd-kit/sortable";
+import type { Column, Id, Task } from "./KanbanBoard";
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Label } from "@/components/ui/label";
+import { Input } from "@/components/ui/input";
+import { Separator } from "@/components/ui/separator";
+import { TaskCard } from "./TaskCard";
+import { TagsCombobox } from "./TagsCombobox";
+import { AssigneesCombobox } from "./AssigneesCombobox";
+import { Calendar } from "@/components/ui/calendar";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { DescriptionEditor } from "./MarkdownEditor";
+import { useDroppable } from "@dnd-kit/core";
+
+const strokeColors = [
+ "stroke-[#25292E]",
+ "stroke-[#1D76DD]",
+ "stroke-[#1A7F37]",
+ "stroke-[#9A6700]",
+ "stroke-[#BC4C00]",
+ "stroke-[#DA4E57]",
+ "stroke-[#BF3989]",
+ "stroke-[#8250DF]",
+];
+
+interface Props {
+ column: Column;
+ deleteColumn: (id: Id) => void;
+ createTask: (
+ columnId: Id,
+ title: string,
+ description: string,
+ startDate?: Date,
+ endDate?: Date,
+ priority?: "High" | "Medium" | "Low",
+ tags?: string[],
+ assignees?: string[],
+ attachments?: File[]
+ ) => void;
+ deleteTask: (id: Id) => void;
+ tasks: Task[];
+ updateTask: (
+ taskId: Id,
+ title: string,
+ description: string,
+ startDate?: Date,
+ endDate?: Date,
+ priority?: "High" | "Medium" | "Low",
+ tags?: string[],
+ assignees?: string[],
+ attachments?: File[]
+ ) => void;
+ editingTask: Task | null;
+ setEditingTask: (task: Task | null) => void;
+ setEditingColumn: (column: Column | null) => void;
+ moveColumnLeft: (id: Id) => void;
+ moveColumnRight: (id: Id) => void;
+ columnIndex: number;
+ totalColumns: number;
+}
+
+export function ColumnContainer({
+ column,
+ deleteColumn,
+ createTask,
+ deleteTask,
+ tasks,
+ updateTask,
+ editingTask,
+ setEditingTask,
+ setEditingColumn,
+ moveColumnLeft,
+ moveColumnRight,
+ columnIndex,
+ totalColumns,
+}: Props) {
+ const [taskTitle, setTaskTitle] = useState("");
+ const [taskDescription, setTaskDescription] = useState("");
+ const [startOpen, setStartOpen] = useState(false);
+ const [endOpen, setEndOpen] = useState(false);
+ const [startDate, setStartDate] = useState(undefined);
+ const [endDate, setEndDate] = useState(undefined);
+ const [isOpen, setIsOpen] = useState(false);
+ const [priority, setPriority] = useState<"High" | "Medium" | "Low">("Medium");
+ const [tags, setTags] = useState([]);
+ const [assignees, setAssignees] = useState([]);
+ const [attachments, setAttachments] = useState([]);
+
+ const tasksIds = useMemo(() => tasks.map((t) => t.id), [tasks]);
+
+ // Reset all form fields
+ const resetForm = useCallback(() => {
+ setTaskTitle("");
+ setTaskDescription("");
+ setStartDate(undefined);
+ setEndDate(undefined);
+ setPriority("Medium");
+ setTags([]);
+ setAssignees([]);
+ setAttachments([]);
+ }, []);
+
+ // Populates dialog with data when editing. Adjust when adding features to dialog.
+ useEffect(() => {
+ if (!editingTask || editingTask.columnId !== column.id) return;
+ setTaskTitle(editingTask.content);
+ setTaskDescription(editingTask.taskDescription);
+ setStartDate(editingTask.startDate);
+ setEndDate(editingTask.endDate);
+ setPriority(editingTask.priority ?? "Medium");
+ setTags(editingTask.tags ?? []);
+ setAssignees(editingTask.assignees ?? []);
+ setAttachments(editingTask.attachments ?? []);
+ setIsOpen(true);
+ }, [editingTask, column.id]);
+
+ // When clicking out of dialog without creation makes sure to clear inputs
+ useEffect(() => {
+ if (!isOpen) {
+ setEditingTask(null);
+ resetForm();
+ }
+ }, [isOpen, resetForm, setEditingTask]);
+
+ const { setNodeRef } = useDroppable({
+ id: column.id,
+ data: { type: "Column", column },
+ });
+
+ // Handler for creating and updating tasks.
+ const handleSubmit = useCallback(() => {
+ if (editingTask) {
+ updateTask(
+ editingTask.id,
+ taskTitle,
+ taskDescription,
+ startDate,
+ endDate,
+ priority,
+ tags,
+ assignees,
+ attachments
+ );
+ setEditingTask(null);
+ } else {
+ createTask(
+ column.id,
+ taskTitle,
+ taskDescription,
+ startDate,
+ endDate,
+ priority,
+ tags,
+ assignees,
+ attachments
+ );
+ }
+ resetForm();
+ setIsOpen(false);
+ }, [
+ editingTask,
+ updateTask,
+ createTask,
+ column.id,
+ taskTitle,
+ taskDescription,
+ startDate,
+ endDate,
+ priority,
+ tags,
+ assignees,
+ attachments,
+ resetForm,
+ setEditingTask,
+ ]);
+
+ return (
+
+ {/* Column Header with drag listeners attached */}
+
+
+
+
+
+ {/* Column Title and Description Display */}
+
+
+ {column.title}
+
+
+ {column.description}
+
+
+
+ {/* Dropdown Button for Editing, Deleting, and Moving Columns*/}
+
+
+
+
+
+
+ Column
+
+ {columnIndex > 0 && (
+ moveColumnLeft(column.id)}
+ className="cursor-pointer"
+ >
+ Move Left
+
+ )}
+ {columnIndex < totalColumns - 1 && (
+ moveColumnRight(column.id)}
+ className="cursor-pointer"
+ >
+ Move Right
+
+ )}
+
+ setEditingColumn(column)} className="cursor-pointer">
+
+ Edit
+
+ {
+ deleteColumn(column.id);
+ }}
+ className="cursor-pointer"
+ >
+
+ Delete
+
+
+
+
+
+ {/* Task List through sortable */}
+
+
+
+ {tasks.map((task) => (
+
+ ))}
+
+
+
+ {/* Task Creation Dialog */}
+
+
+ );
+}
diff --git a/frontend/src/components/ui/task-components/Filter.tsx b/frontend/src/components/ui/task-components/Filter.tsx
new file mode 100644
index 0000000..3464d84
--- /dev/null
+++ b/frontend/src/components/ui/task-components/Filter.tsx
@@ -0,0 +1,435 @@
+// Filter Command Bar for the KanbanBoard
+// Allows for multiselection filtering
+
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command";
+import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
+import { Check, Tag, User, ListFilter, Calendar, Hash, X } from "lucide-react";
+import type { Column, Task } from "./KanbanBoard";
+import { useState, useMemo } from "react";
+import { Button } from "../button";
+import { cn } from "@/lib/utils";
+import { Badge } from "@/components/ui/badge";
+
+interface Props {
+ columns: Column[];
+ tasks: Task[];
+ filters: {
+ statuses: string[];
+ assignees: string[];
+ priorities: string[];
+ labels: string[];
+ startDates: string[];
+ endDates: string[];
+ };
+ updateFilters: (key: keyof Props["filters"], value: string[]) => void;
+}
+
+export function FilterCommandBar({ columns, tasks, filters, updateFilters }: Props) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [inputValue, setInputValue] = useState("");
+
+ const toggleFilter = (filterKey: keyof Props["filters"], filterValue: string) => {
+ const currentFilterList = filters[filterKey];
+ if (currentFilterList.includes(filterValue)) {
+ updateFilters(
+ filterKey,
+ currentFilterList.filter((value) => value !== filterValue)
+ );
+ } else {
+ updateFilters(filterKey, [...currentFilterList, filterValue]);
+ }
+ };
+
+ const clearAllFilters = () => {
+ updateFilters("statuses", []);
+ updateFilters("assignees", []);
+ updateFilters("priorities", []);
+ updateFilters("labels", []);
+ updateFilters("startDates", []);
+ updateFilters("endDates", []);
+ setInputValue("");
+ };
+
+ const uniqueAssignees = Array.from(new Set(tasks.flatMap((task) => task.assignees || [])));
+ const uniqueLabels = Array.from(new Set(tasks.flatMap((task) => task.tags || [])));
+
+ const uniqueStartDates = Array.from(
+ new Set(
+ tasks
+ .filter((task) => task.startDate)
+ .map((task) =>
+ new Date(task.startDate!).toLocaleDateString("en-US", {
+ month: "2-digit",
+ day: "2-digit",
+ year: "numeric",
+ })
+ )
+ )
+ );
+
+ const uniqueEndDates = Array.from(
+ new Set(
+ tasks
+ .filter((task) => task.endDate)
+ .map((task) =>
+ new Date(task.endDate!).toLocaleDateString("en-US", {
+ month: "2-digit",
+ day: "2-digit",
+ year: "numeric",
+ })
+ )
+ )
+ );
+
+ const getFilterDisplayName = (filterKey: keyof Props["filters"], value: string) => {
+ switch (filterKey) {
+ case "statuses":
+ return columns.find((col) => String(col.id) === value)?.title || value;
+ case "assignees":
+ case "labels":
+ case "startDates":
+ case "endDates":
+ return value;
+ case "priorities":
+ return value;
+ default:
+ return value;
+ }
+ };
+
+ const activeFilterTokens = [
+ ...filters.statuses.map((value) => ({
+ key: "statuses" as const,
+ value,
+ display: `status:${getFilterDisplayName("statuses", value)}`,
+ })),
+ ...filters.assignees.map((value) => ({
+ key: "assignees" as const,
+ value,
+ display: `assignee:${value}`,
+ })),
+ ...filters.priorities.map((value) => ({
+ key: "priorities" as const,
+ value,
+ display: `priority:${value.toLowerCase()}`,
+ })),
+ ...filters.labels.map((value) => ({
+ key: "labels" as const,
+ value,
+ display: `label:${value}`,
+ })),
+ ...filters.startDates.map((value) => ({
+ key: "startDates" as const,
+ value,
+ display: `start:${value}`,
+ })),
+ ...filters.endDates.map((value) => ({
+ key: "endDates" as const,
+ value,
+ display: `due:${value}`,
+ })),
+ ];
+
+ const parsedInput = useMemo(() => {
+ const trimmedInput = inputValue.trim().toLowerCase();
+
+ if (!trimmedInput) {
+ return { type: "all", query: "", showGroupSuggestions: true };
+ }
+
+ if (trimmedInput.includes(":")) {
+ const [prefix, query] = trimmedInput.split(":");
+ return {
+ type: prefix,
+ query: query || "",
+ showGroupSuggestions: !query,
+ };
+ }
+ return { type: "search", query: trimmedInput, showGroupSuggestions: true };
+ }, [inputValue]);
+
+ const filterPrefixes = [
+ { value: "status:", label: "Status", icon: Tag, description: "Filter by task status" },
+ { value: "assignee:", label: "Assignee", icon: User, description: "Filter by assignee" },
+ { value: "priority:", label: "Priority", icon: Tag, description: "Filter by priority level" },
+ { value: "label:", label: "Label", icon: Hash, description: "Filter by label" },
+ { value: "start:", label: "Start Date", icon: Calendar, description: "Filter by start date" },
+ { value: "due:", label: "Due Date", icon: Calendar, description: "Filter by due date" },
+ ];
+
+ const filteredGroupSuggestions = useMemo(() => {
+ const { query } = parsedInput;
+ if (!query) return filterPrefixes;
+ return filterPrefixes.filter(
+ (prefix) =>
+ prefix.value.toLowerCase().includes(query) || prefix.label.toLowerCase().includes(query)
+ );
+ }, [parsedInput]);
+
+ const filteredSuggestions = useMemo(() => {
+ const { type, query } = parsedInput;
+
+ const suggestions = {
+ statuses: columns.filter((col) =>
+ type === "all" || type === "status" || type === "search"
+ ? col.title.toLowerCase().includes(query)
+ : false
+ ),
+ assignees: uniqueAssignees.filter((assignee) =>
+ type === "all" || type === "assignee" || type === "search"
+ ? assignee.toLowerCase().includes(query)
+ : false
+ ),
+ priorities: (["High", "Medium", "Low"] as const).filter((priority) =>
+ type === "all" || type === "priority" || type === "search"
+ ? priority.toLowerCase().includes(query)
+ : false
+ ),
+ labels: uniqueLabels.filter((label) =>
+ type === "all" || type === "label" || type === "search"
+ ? label.toLowerCase().includes(query)
+ : false
+ ),
+ startDates: uniqueStartDates.filter((date) =>
+ type === "all" || type === "start" || type === "search"
+ ? date.toLowerCase().includes(query)
+ : false
+ ),
+ endDates: uniqueEndDates.filter((date) =>
+ type === "all" || type === "due" || type === "search"
+ ? date.toLowerCase().includes(query)
+ : false
+ ),
+ };
+ return suggestions;
+ }, [parsedInput, columns, uniqueAssignees, uniqueLabels, uniqueStartDates, uniqueEndDates]);
+
+ const handleSelectSuggestion = (filterKey: keyof Props["filters"], value: string) => {
+ toggleFilter(filterKey, value);
+ setInputValue("");
+ };
+
+ const handleSelectPrefix = (prefix: string) => {
+ setInputValue(prefix);
+ };
+
+ return (
+
+
+
+
+
+
+ {activeFilterTokens.length > 0 ? (
+
+ {activeFilterTokens.map(({ key, value, display }) => (
+ e.stopPropagation()}
+ >
+ {display}
+
+
+ ))}
+
+ ) : (
+
Filter tasks…
+ )}
+
+
+
+
+
+
+ No results found.
+ {parsedInput.showGroupSuggestions && filteredGroupSuggestions.length > 0 && (
+
+ {filteredGroupSuggestions.map((prefix) => (
+ handleSelectPrefix(prefix.value)}
+ className="flex items-center gap-2"
+ >
+
+
+ {prefix.label}
+
+ {prefix.description}
+
+
+
+ {prefix.value}
+
+
+ ))}
+
+ )}
+ {filteredSuggestions.statuses.length > 0 && parsedInput.type === "status" && (
+
+ {filteredSuggestions.statuses.map((column) => (
+ handleSelectSuggestion("statuses", String(column.id))}
+ className="flex items-center gap-2"
+ >
+ {filters.statuses.includes(String(column.id)) ? (
+
+ ) : (
+
+ )}
+
+ {column.title}
+
+ status:{column.title.toLowerCase()}
+
+
+ ))}
+
+ )}
+ {filteredSuggestions.assignees.length > 0 && parsedInput.type === "assignee" && (
+
+ {filteredSuggestions.assignees.map((assigneeName) => (
+ handleSelectSuggestion("assignees", assigneeName)}
+ className="flex items-center gap-2"
+ >
+ {filters.assignees.includes(assigneeName) ? (
+
+ ) : (
+
+ )}
+
+ {assigneeName}
+
+ assignee:{assigneeName}
+
+
+ ))}
+
+ )}
+ {filteredSuggestions.priorities.length > 0 && parsedInput.type === "priority" && (
+
+ {filteredSuggestions.priorities.map((priorityLevel) => (
+ handleSelectSuggestion("priorities", priorityLevel)}
+ className="flex items-center gap-2"
+ >
+ {filters.priorities.includes(priorityLevel) ? (
+
+ ) : (
+
+ )}
+
+ {priorityLevel}
+
+ priority:{priorityLevel.toLowerCase()}
+
+
+ ))}
+
+ )}
+ {filteredSuggestions.labels.length > 0 && parsedInput.type === "label" && (
+
+ {filteredSuggestions.labels.map((labelName) => (
+ handleSelectSuggestion("labels", labelName)}
+ className="flex items-center gap-2"
+ >
+ {filters.labels.includes(labelName) ? (
+
+ ) : (
+
+ )}
+
+ {labelName}
+
+ label:{labelName}
+
+
+ ))}
+
+ )}
+ {filteredSuggestions.startDates.length > 0 && parsedInput.type === "start" && (
+
+ {filteredSuggestions.startDates.map((startDate) => (
+ handleSelectSuggestion("startDates", startDate)}
+ className="flex items-center gap-2"
+ >
+ {filters.startDates.includes(startDate) ? (
+
+ ) : (
+
+ )}
+
+ {startDate}
+
+ start:{startDate}
+
+
+ ))}
+
+ )}
+ {filteredSuggestions.endDates.length > 0 && parsedInput.type === "due" && (
+
+ {filteredSuggestions.endDates.map((dueDate) => (
+ handleSelectSuggestion("endDates", dueDate)}
+ className="flex items-center gap-2"
+ >
+ {filters.endDates.includes(dueDate) ? (
+
+ ) : (
+
+ )}
+
+ {dueDate}
+ due:{dueDate}
+
+ ))}
+
+ )}
+
+
+
+
+ {activeFilterTokens.length > 0 && (
+
+ )}
+
+
+ );
+}
diff --git a/frontend/src/components/ui/task-components/KanbanBoard.tsx b/frontend/src/components/ui/task-components/KanbanBoard.tsx
new file mode 100644
index 0000000..d63508f
--- /dev/null
+++ b/frontend/src/components/ui/task-components/KanbanBoard.tsx
@@ -0,0 +1,499 @@
+// Board that contains all of the Tasks and Columns and their implementations
+// Contains the dialog for column creation
+// Contains functions for Dragging, Creation of Task and Columns, Moving of Columns, Updating and Editing Tasks and Columns
+
+import { Button } from "@/components/ui/button";
+import { Label } from "@/components/ui/label";
+import { Input } from "@/components/ui/input";
+import { Separator } from "@/components/ui/separator";
+import { Textarea } from "@/components/ui/textarea";
+import { Plus, Circle } from "lucide-react";
+import { useState, useMemo, useEffect } from "react";
+import { ColumnContainer } from "./ColumnContainer";
+import {
+ DndContext,
+ DragOverlay,
+ useSensors,
+ useSensor,
+ type DragEndEvent,
+ type DragStartEvent,
+ PointerSensor,
+ type DragOverEvent,
+} from "@dnd-kit/core";
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { SortableContext, arrayMove } from "@dnd-kit/sortable";
+import { TaskCard } from "./TaskCard";
+import { dummyColumns, dummyTasks } from "@/lib/dummyData/dummyTasks";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+
+const buttonColors = [
+ "bg-gray-600",
+ "bg-blue-400",
+ "bg-green-500",
+ "bg-yellow-500",
+ "bg-orange-500",
+ "bg-red-500",
+ "bg-pink-500",
+ "bg-purple-500",
+];
+
+const strokeColors = [
+ "stroke-gray-900",
+ "stroke-blue-900",
+ "stroke-green-900",
+ "stroke-yellow-900",
+ "stroke-orange-900",
+ "stroke-red-900",
+ "stroke-pink-900",
+ "stroke-purple-900",
+];
+
+export type Id = string | number;
+
+export type Column = {
+ id: Id;
+ color: string;
+ colorIndex: number;
+ title: string;
+ description: string;
+};
+
+export type Task = {
+ id: Id;
+ columnId: Id;
+ content: string;
+ taskDescription: string;
+ startDate?: Date;
+ endDate?: Date;
+ priority?: "High" | "Medium" | "Low";
+ tags?: string[];
+ assignees?: string[];
+ attachments?: File[];
+};
+
+interface KanbanBoardProps {
+ columns: Column[];
+ setColumns: React.Dispatch>;
+ tasks: Task[];
+ setTasks: React.Dispatch>;
+}
+
+export function KanbanBoard({ columns, setColumns, tasks, setTasks }: KanbanBoardProps) {
+ const [userTitle, setUserTitle] = useState("");
+ const [userDescription, setUserDescription] = useState("");
+ const [isSelected, setIsSelected] = useState(0);
+ const [columnColor, setColumnColor] = useState(buttonColors[0]);
+ const columnsId = useMemo(() => columns.map((col) => col.id), [columns]);
+ const [activeTask, setActiveTask] = useState(null);
+ const [editingTask, setEditingTask] = useState(null);
+ const [editingColumn, setEditingColumn] = useState(null);
+ const [columnDialogOpen, setColumnDialogOpen] = useState(false);
+
+ const resetToDefaultData = () => {
+ setColumns(dummyColumns);
+ setTasks(dummyTasks);
+ };
+
+ const sensors = useSensors(
+ useSensor(PointerSensor, {
+ activationConstraint: {
+ distance: 3,
+ },
+ })
+ );
+
+ useEffect(() => {
+ if (editingColumn) {
+ setUserTitle(editingColumn.title);
+ setUserDescription(editingColumn.description);
+ setColumnColor(editingColumn.color);
+ setIsSelected(editingColumn.colorIndex);
+ setColumnDialogOpen(true);
+ }
+ }, [editingColumn]);
+ useEffect(() => {
+ if (!columnDialogOpen) {
+ setUserTitle("");
+ setUserDescription("");
+ setIsSelected(0);
+ setColumnColor(buttonColors[0]);
+ setColumnDialogOpen(false);
+ setEditingColumn(null);
+ }
+ }, [columnDialogOpen]);
+
+ return (
+
+
+
+
+
+
+ {activeTask && (
+ c.id === activeTask.columnId) as Column}
+ deleteTask={() => {}}
+ setEditingTask={() => {}}
+ />
+ )}
+
+
+ );
+
+ function createTask(
+ columnId: Id,
+ title: string,
+ description: string,
+ startDate?: Date,
+ endDate?: Date,
+ priority?: "High" | "Medium" | "Low",
+ tags?: string[],
+ assignees?: string[],
+ attachments?: File[]
+ ) {
+ const newTask: Task = {
+ id: generatedId(),
+ columnId,
+ content: title,
+ taskDescription: description,
+ startDate,
+ endDate,
+ priority: priority || "Medium",
+ tags: tags || [],
+ assignees: assignees || [],
+ attachments: attachments || [],
+ };
+ setTasks([...tasks, newTask]);
+ }
+
+ function deleteTask(id: Id) {
+ const newTasks = tasks.filter((task) => task.id !== id);
+ setTasks(newTasks);
+ }
+
+ function createNewColumn() {
+ const columnToAdd: Column = {
+ id: generatedId(),
+ color: columnColor,
+ colorIndex: isSelected,
+ title: userTitle,
+ description: userDescription,
+ };
+ setIsSelected(0);
+ setColumnColor(buttonColors[0]);
+ setColumns([...columns, columnToAdd]);
+ setUserTitle("");
+ setUserDescription("");
+ }
+
+ function handleColumnSubmit() {
+ if (editingColumn) {
+ updateColumn(editingColumn.id, userTitle, userDescription, columnColor, isSelected);
+ setEditingColumn(null);
+ } else {
+ createNewColumn();
+ }
+ setUserTitle("");
+ setUserDescription("");
+ setIsSelected(0);
+ setColumnColor(buttonColors[0]);
+ setColumnDialogOpen(false);
+ setEditingColumn(null);
+ }
+
+ function deleteColumn(id: Id) {
+ const filteredColumns = columns.filter((col) => col.id !== id);
+ setColumns(filteredColumns);
+
+ const newTasks = tasks.filter((task) => task.columnId !== id);
+ setTasks(newTasks);
+ }
+
+ function onDragStart(event: DragStartEvent) {
+ if (event.active.data.current?.type === "Task") {
+ setActiveTask(event.active.data.current.task);
+ return;
+ }
+ }
+
+ function onDragEnd(event: DragEndEvent) {
+ setActiveTask(null);
+
+ const { active, over } = event;
+ if (!over) return;
+
+ const activeId = active.id;
+ const overId = over.id;
+
+ if (activeId === overId) return;
+
+ const isActiveAColumn = active.data.current?.type === "Column";
+ const isOverAColumn = over.data.current?.type === "Column";
+
+ if (isActiveAColumn && isOverAColumn) {
+ setColumns((columns) => {
+ const activeColumnIndex = columns.findIndex((col) => col.id === activeId);
+ const overColumnIndex = columns.findIndex((col) => col.id === overId);
+
+ if (activeColumnIndex === -1 || overColumnIndex === -1) return columns;
+
+ return arrayMove(columns, activeColumnIndex, overColumnIndex);
+ });
+ }
+ }
+
+ function onDragOver(event: DragOverEvent) {
+ const { active, over } = event;
+ if (!over) return;
+
+ const activeTaskId = active.id;
+ const overTaskId = over.id;
+
+ if (activeTaskId === overTaskId) return;
+
+ const isActiveATask = active.data.current?.type === "Task";
+ const isOverATask = over.data.current?.type === "Task";
+
+ if (!isActiveATask) return;
+
+ if (isActiveATask && isOverATask) {
+ setTasks((tasks) => {
+ const activeIndex = tasks.findIndex((task) => task.id === activeTaskId);
+ const overIndex = tasks.findIndex((task) => task.id === overTaskId);
+
+ tasks[activeIndex].columnId = tasks[overIndex].columnId;
+
+ return arrayMove(tasks, activeIndex, overIndex);
+ });
+ }
+
+ const isOverAColumn = over.data.current?.type === "Column";
+
+ if (isActiveATask && isOverAColumn) {
+ setTasks((tasks) => {
+ const activeIndex = tasks.findIndex((task) => task.id === activeTaskId);
+
+ tasks[activeIndex].columnId = overTaskId;
+
+ return arrayMove(tasks, activeIndex, activeIndex);
+ });
+ }
+ }
+
+ function selectedButton(color: string, index: number) {
+ setIsSelected(index);
+ setColumnColor(color);
+ }
+
+ function updateTask(
+ taskId: Id,
+ title: string,
+ description: string,
+ startDate?: Date,
+ endDate?: Date,
+ priority?: "High" | "Medium" | "Low",
+ tags?: string[],
+ assignees?: string[],
+ attachments?: File[]
+ ) {
+ setTasks(
+ tasks.map((task) =>
+ task.id === taskId
+ ? {
+ ...task,
+ content: title,
+ taskDescription: description,
+ startDate,
+ endDate,
+ priority,
+ tags,
+ assignees,
+ attachments,
+ }
+ : task
+ )
+ );
+ }
+
+ function updateColumn(
+ columnId: Id,
+ title: string,
+ description: string,
+ color: string,
+ colorIndex: number
+ ) {
+ setColumns(
+ columns.map((col) =>
+ col.id === columnId ? { ...col, title, description, color, colorIndex } : col
+ )
+ );
+ }
+
+ function moveColumnLeft(id: Id) {
+ setColumns((prev) => {
+ const index = prev.findIndex((col) => col.id === id);
+ if (index <= 0) return prev;
+
+ const newCols = [...prev];
+ [newCols[index - 1], newCols[index]] = [newCols[index], newCols[index - 1]];
+ return newCols;
+ });
+ }
+
+ function moveColumnRight(id: Id) {
+ setColumns((prev) => {
+ const index = prev.findIndex((col) => col.id === id);
+ if (index === -1 || index >= prev.length - 1) return prev;
+
+ const newCols = [...prev];
+ [newCols[index], newCols[index + 1]] = [newCols[index + 1], newCols[index]];
+ return newCols;
+ });
+ }
+}
+
+function generatedId() {
+ return Date.now();
+}
diff --git a/frontend/src/components/ui/task-components/LearnMore.tsx b/frontend/src/components/ui/task-components/LearnMore.tsx
new file mode 100644
index 0000000..7ad99aa
--- /dev/null
+++ b/frontend/src/components/ui/task-components/LearnMore.tsx
@@ -0,0 +1,47 @@
+// Learn More Button
+// Shows Dialog for Project Description
+
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Separator } from "@/components/ui/separator";
+
+const dummyText =
+ "Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem";
+
+export function LearnMore() {
+ return (
+
+
+
+ );
+}
diff --git a/frontend/src/components/ui/task-components/MarkdownEditor.tsx b/frontend/src/components/ui/task-components/MarkdownEditor.tsx
new file mode 100644
index 0000000..4b7f454
--- /dev/null
+++ b/frontend/src/components/ui/task-components/MarkdownEditor.tsx
@@ -0,0 +1,102 @@
+// Task Description component for previewing and displaying unique description creation
+
+import { useState, useRef } from "react";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Textarea } from "@/components/ui/textarea";
+import { Button } from "@/components/ui/button";
+import ReactMarkdown from "react-markdown";
+import { Bold, Italic, Heading } from "lucide-react";
+import remarkGfm from "remark-gfm";
+
+interface Props {
+ value: string;
+ onChange: (value: string) => void;
+}
+
+export function DescriptionEditor({ value, onChange }: Props) {
+ const [activeTab, setActiveTab] = useState("write");
+ const textareaRef = useRef(null);
+
+ const insertMarkdown = (syntax: string, placeholder = "") => {
+ const textarea = textareaRef.current;
+ if (!textarea) return;
+
+ const start = textarea.selectionStart;
+ const end = textarea.selectionEnd;
+
+ const selectedText = value.substring(start, end) || placeholder;
+ const markdown = syntax.replace("{}", selectedText);
+
+ const newText = value.substring(0, start) + markdown + value.substring(end);
+
+ onChange(newText);
+
+ setTimeout(() => {
+ textarea.focus();
+ textarea.selectionStart = textarea.selectionEnd = start + markdown.length;
+ }, 0);
+ };
+
+ return (
+
+
+
+
+
+ Write
+
+
+ Preview
+
+
+ {activeTab === "write" && (
+
+
+
+
+
+ )}
+
+
+
+
+ {value ? (
+
+ {value}
+
+ ) : (
+ Nothing to preview
+ )}
+
+
+
+ );
+}
diff --git a/frontend/src/components/ui/task-components/MyTaskContent.tsx b/frontend/src/components/ui/task-components/MyTaskContent.tsx
new file mode 100644
index 0000000..f346236
--- /dev/null
+++ b/frontend/src/components/ui/task-components/MyTaskContent.tsx
@@ -0,0 +1,356 @@
+// Sidebar for MyTask Tab filtering
+
+import { FilterDropDown } from "@/components/ui/task-components/MyTaskDropdown";
+import { Card } from "@/components/ui/card";
+import type { Column, Id, Task } from "./KanbanBoard";
+import { useMemo, useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Calendar } from "lucide-react";
+import { priorityLevels, availableTags, teamMembers } from "@/lib/dummyData/dummyTasks";
+
+interface Props {
+ columns: Column[];
+ tasks: Task[];
+ onColumnSelect: (columnId: Id | null) => void;
+ onAssigneeSelect: (assignee: string | null) => void;
+ onPrioritySelect: (priority: "High" | "Medium" | "Low" | null) => void;
+ onStartDateSelect: (date: string | null) => void;
+ onEndDateSelect: (date: string | null) => void;
+ onLabelSelect: (tag: string | null) => void;
+ selectedColumnId: Id | null;
+ selectedAssignee: string | null;
+ selectedPriority: "High" | "Medium" | "Low" | null;
+ selectedStartDate: string | null;
+ selectedEndDate: string | null;
+ selectedLabel: string | null;
+}
+
+export function MyTask(props: Props) {
+ const {
+ columns,
+ tasks,
+ onColumnSelect,
+ onAssigneeSelect,
+ onPrioritySelect,
+ onStartDateSelect,
+ onEndDateSelect,
+ onLabelSelect,
+ selectedColumnId,
+ selectedAssignee,
+ selectedPriority,
+ selectedStartDate,
+ selectedEndDate,
+ selectedLabel,
+ } = props;
+
+ const [selectedFilter, setSelectedFilter] = useState("Status");
+
+ const getAssigneeColor = useMemo(() => {
+ return (assignee: string) => {
+ const member = teamMembers.find((member) => member.value === assignee);
+ return member?.color || "from-gray-400 to-gray-600";
+ };
+ }, []);
+
+ const uniqueAssignees = useMemo(() => {
+ const assigneeSet = new Set();
+ tasks.forEach((task) => {
+ task.assignees?.forEach((assignee) => assigneeSet.add(assignee));
+ });
+ return Array.from(assigneeSet).sort();
+ }, [tasks]);
+
+ const uniqueStartDates = useMemo(() => {
+ const dateSet = new Set();
+ tasks.forEach((task) => {
+ if (task.startDate) {
+ const dateString = new Date(task.startDate).toLocaleDateString("en-US", {
+ month: "2-digit",
+ day: "2-digit",
+ year: "numeric",
+ });
+ dateSet.add(dateString);
+ }
+ });
+ return Array.from(dateSet).sort();
+ }, [tasks]);
+
+ const uniqueEndDates = useMemo(() => {
+ const dateSet = new Set();
+ tasks.forEach((task) => {
+ if (task.endDate) {
+ const dateString = new Date(task.endDate).toLocaleDateString("en-US", {
+ month: "2-digit",
+ day: "2-digit",
+ year: "numeric",
+ });
+ dateSet.add(dateString);
+ }
+ });
+ return Array.from(dateSet).sort();
+ }, [tasks]);
+
+ const uniqueTags = useMemo(() => {
+ const tagSet = new Set();
+ tasks.forEach((task) => {
+ task.tags?.forEach((tag) => tagSet.add(tag));
+ });
+ return Array.from(tagSet).sort();
+ }, [tasks]);
+
+ const handleFilterChange = (value: string) => {
+ setSelectedFilter(value);
+ onColumnSelect(null);
+ onAssigneeSelect(null);
+ onPrioritySelect(null);
+ onStartDateSelect(null);
+ onEndDateSelect(null);
+ onLabelSelect(null);
+ };
+
+ const handleClearSelection = () => {
+ onColumnSelect(null);
+ onAssigneeSelect(null);
+ onPrioritySelect(null);
+ onStartDateSelect(null);
+ onEndDateSelect(null);
+ onLabelSelect(null);
+ };
+
+ return (
+
+
+ {selectedFilter === "Status" && (
+
+
+ {columns.map((column) => {
+ const columnTasks = tasks.filter((task) => task.columnId === column.id);
+ const isSelected = selectedColumnId === column.id;
+ return (
+
onColumnSelect(isSelected ? null : column.id)}
+ >
+
+
+ {columnTasks.length} {columnTasks.length === 1 ? "task" : "tasks"}
+
+
+ );
+ })}
+
+
+ )}
+ {selectedFilter === "Assignee" && (
+
+
+ {uniqueAssignees.map((assignee) => {
+ const assigneeTasks = tasks.filter((task) => task.assignees?.includes(assignee));
+ const isSelected = selectedAssignee === assignee;
+ return (
+
onAssigneeSelect(isSelected ? null : assignee)}
+ >
+
+
+ {assignee.charAt(0).toUpperCase()}
+
+
{assignee}
+
+
+ {assigneeTasks.length} {assigneeTasks.length === 1 ? "task" : "tasks"}
+
+
+ );
+ })}
+ {uniqueAssignees.length === 0 && (
+
No assignees found
+ )}
+
+
+ )}
+ {selectedFilter === "Priority" && (
+
+
+ {priorityLevels.map((priority) => {
+ const priorityTasks = tasks.filter((task) => task.priority === priority.value);
+ const isSelected = selectedPriority === priority.value;
+ return (
+
+ onPrioritySelect(
+ isSelected ? null : (priority.value as "High" | "Medium" | "Low")
+ )
+ }
+ >
+
+
+ {priorityTasks.length} {priorityTasks.length === 1 ? "task" : "tasks"}
+
+
+ );
+ })}
+
+
+ )}
+ {selectedFilter === "Start Date" && (
+
+
+ {uniqueStartDates.map((date) => {
+ const dateTasks = tasks.filter((task) => {
+ if (!task.startDate) return false;
+ const taskDateString = new Date(task.startDate).toLocaleDateString("en-US", {
+ month: "2-digit",
+ day: "2-digit",
+ year: "numeric",
+ });
+ return taskDateString === date;
+ });
+ const isSelected = selectedStartDate === date;
+ return (
+
onStartDateSelect(isSelected ? null : date)}
+ >
+
+
+
{date}
+
+
+ {dateTasks.length} {dateTasks.length === 1 ? "task" : "tasks"}
+
+
+ );
+ })}
+ {uniqueStartDates.length === 0 && (
+
No start dates found
+ )}
+
+
+ )}
+ {selectedFilter === "End Date" && (
+
+
+ {uniqueEndDates.map((date) => {
+ const dateTasks = tasks.filter((task) => {
+ if (!task.endDate) return false;
+ const taskDateString = new Date(task.endDate).toLocaleDateString("en-US", {
+ month: "2-digit",
+ day: "2-digit",
+ year: "numeric",
+ });
+ return taskDateString === date;
+ });
+ const isSelected = selectedEndDate === date;
+ return (
+
onEndDateSelect(isSelected ? null : date)}
+ >
+
+
+
{date}
+
+
+ {dateTasks.length} {dateTasks.length === 1 ? "task" : "tasks"}
+
+
+ );
+ })}
+ {uniqueEndDates.length === 0 && (
+
No end dates found
+ )}
+
+
+ )}
+ {selectedFilter === "Label" && (
+
+
+ {uniqueTags.map((tagValue) => {
+ const tag = availableTags.find((tag) => tag.value === tagValue);
+ const tagTasks = tasks.filter((task) => task.tags?.includes(tagValue));
+ const isSelected = selectedLabel === tagValue;
+ const tagLabel = tag?.label || tagValue;
+ const tagColor = tag?.color || "bg-gray-500";
+ return (
+
onLabelSelect(isSelected ? null : tagValue)}
+ >
+
+ {tagTasks.length > 0 && (
+
+ {Array.from(new Set(tagTasks.flatMap((tag) => tag.assignees || [])))
+ .slice(0, 5)
+ .map((assignee) => (
+
+ {assignee.charAt(0).toUpperCase()}
+
+ ))}
+ {Array.from(new Set(tagTasks.flatMap((tag) => tag.assignees || []))).length >
+ 5 && (
+
+ +
+ {Array.from(new Set(tagTasks.flatMap((tag) => tag.assignees || [])))
+ .length - 5}
+
+ )}
+
+ )}
+
+ );
+ })}
+ {uniqueTags.length === 0 && (
+
No tags found
+ )}
+
+
+ )}
+
+
+ );
+}
diff --git a/frontend/src/components/ui/task-components/MyTaskDropdown.tsx b/frontend/src/components/ui/task-components/MyTaskDropdown.tsx
new file mode 100644
index 0000000..21e3a4a
--- /dev/null
+++ b/frontend/src/components/ui/task-components/MyTaskDropdown.tsx
@@ -0,0 +1,54 @@
+// MyTaskContent Filter Dropdown component
+// Handles different selections for filtering
+
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuLabel,
+ DropdownMenuTrigger,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+} from "@/components/ui/dropdown-menu";
+import { LucideChevronDown } from "lucide-react";
+
+interface FilterDropDownProps {
+ onValueChange: (value: string) => void;
+ selectedValue: string | null;
+}
+
+export function FilterDropDown({ onValueChange, selectedValue }: FilterDropDownProps) {
+ return (
+
+
+
+
+
+ Slice by
+
+
+ Status
+
+
+ Assignee
+
+
+ Priority
+
+
+ Start Date
+
+
+ End Date
+
+
+ Label
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/ui/task-components/MyTaskTable.tsx b/frontend/src/components/ui/task-components/MyTaskTable.tsx
new file mode 100644
index 0000000..de63e5c
--- /dev/null
+++ b/frontend/src/components/ui/task-components/MyTaskTable.tsx
@@ -0,0 +1,298 @@
+// Task Table that contains all of the information displayed in the Kanban Board
+// With the integration of the MyTaskContent, allows for specific filtering of the Task and Columns in the form of a table
+
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { useMemo, useState } from "react";
+import { ChevronDown, ChevronRight, Plus } from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import type { Column, Id, Task } from "./KanbanBoard";
+import { TaskSheet } from "./TaskSheet";
+import { availableTags, priorityStyles } from "@/lib/dummyData/dummyTasks";
+
+interface Props {
+ columns: Column[];
+ tasks: Task[];
+ selectedColumnId?: Id | null;
+ selectedAssignee?: string | null;
+ selectedPriority?: "High" | "Medium" | "Low" | null;
+ selectedStartDate?: string | null;
+ selectedEndDate?: string | null;
+ selectedLabel?: string | null;
+}
+
+export function TaskTable({
+ columns,
+ tasks,
+ selectedColumnId,
+ selectedAssignee,
+ selectedPriority,
+ selectedStartDate,
+ selectedEndDate,
+ selectedLabel,
+}: Props) {
+ const [expandedGroups, setExpandedGroups] = useState>(() => {
+ const initial: Record = {};
+ columns.forEach((col) => {
+ initial[col.id.toString()] = true;
+ });
+ return initial;
+ });
+
+ const formatDateForComparison = (date: Date) => {
+ return new Date(date).toLocaleDateString("en-US", {
+ month: "2-digit",
+ day: "2-digit",
+ year: "numeric",
+ });
+ };
+
+ const visibleColumns = useMemo(() => {
+ if (selectedColumnId) {
+ return columns.filter((col) => col.id === selectedColumnId);
+ }
+
+ if (selectedAssignee) {
+ return columns.filter((column) => {
+ return tasks.some(
+ (task) => task.columnId === column.id && task.assignees?.includes(selectedAssignee)
+ );
+ });
+ }
+
+ if (selectedPriority) {
+ return columns.filter((column) => {
+ return tasks.some(
+ (task) => task.columnId === column.id && task.priority === selectedPriority
+ );
+ });
+ }
+
+ if (selectedStartDate) {
+ return columns.filter((column) => {
+ return tasks.some(
+ (task) =>
+ task.columnId === column.id &&
+ task.startDate &&
+ formatDateForComparison(task.startDate) === selectedStartDate
+ );
+ });
+ }
+
+ if (selectedEndDate) {
+ return columns.filter((column) => {
+ return tasks.some(
+ (task) =>
+ task.columnId === column.id &&
+ task.endDate &&
+ formatDateForComparison(task.endDate) === selectedEndDate
+ );
+ });
+ }
+
+ if (selectedLabel) {
+ return columns.filter((column) => {
+ return tasks.some(
+ (task) => task.columnId === column.id && task.tags?.includes(selectedLabel)
+ );
+ });
+ }
+ return columns;
+ }, [
+ columns,
+ selectedColumnId,
+ selectedAssignee,
+ selectedPriority,
+ selectedStartDate,
+ selectedEndDate,
+ selectedLabel,
+ tasks,
+ ]);
+
+ const groupedTasks = useMemo(() => {
+ const grouped: Record = {};
+
+ visibleColumns.forEach((column) => {
+ let columnTasks = tasks.filter((task) => task.columnId === column.id);
+
+ if (selectedAssignee) {
+ columnTasks = columnTasks.filter((task) => task.assignees?.includes(selectedAssignee));
+ }
+
+ if (selectedPriority) {
+ columnTasks = columnTasks.filter((task) => task.priority === selectedPriority);
+ }
+
+ if (selectedStartDate) {
+ columnTasks = columnTasks.filter(
+ (task) => task.startDate && formatDateForComparison(task.startDate) === selectedStartDate
+ );
+ }
+
+ if (selectedEndDate) {
+ columnTasks = columnTasks.filter(
+ (task) => task.endDate && formatDateForComparison(task.endDate) === selectedEndDate
+ );
+ }
+
+ if (selectedLabel) {
+ columnTasks = columnTasks.filter((task) => task.tags?.includes(selectedLabel));
+ }
+
+ grouped[column.id.toString()] = columnTasks;
+ });
+
+ return grouped;
+ }, [
+ tasks,
+ visibleColumns,
+ selectedAssignee,
+ selectedPriority,
+ selectedStartDate,
+ selectedEndDate,
+ selectedLabel,
+ ]);
+
+ const toggleGroup = (columnId: string) => {
+ setExpandedGroups((prev) => ({
+ ...prev,
+ [columnId]: !prev[columnId],
+ }));
+ };
+
+ const formatDate = (date?: Date) => {
+ if (!date) return "-";
+ return new Date(date).toLocaleDateString("en-US", {
+ month: "2-digit",
+ day: "2-digit",
+ year: "numeric",
+ });
+ };
+
+ return (
+
+ {visibleColumns.length === 0 && (
+
+ No columns with tasks for the selected filter
+
+ )}
+ {visibleColumns.map((column) => {
+ const columnTasks = groupedTasks[column.id.toString()] || [];
+ const isExpanded = expandedGroups[column.id.toString()];
+ return (
+
+
toggleGroup(column.id.toString())}
+ >
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+
{column.title}
+
+ {columnTasks.length}
+
+
+ {isExpanded && (
+
+
+
+ #
+
+ Title
+
+
+ Priority
+
+
+ Label
+
+
+ Start date
+
+
+ Due date
+
+
+ Status
+
+
+
+
+ {columnTasks.length > 0 ? (
+ <>
+ {columnTasks.map((task, index) => (
+
+
+ {index + 1}
+
+
+
+
+
+ {task.priority && (
+
+ {task.priority}
+
+ )}
+
+
+ {task.tags && task.tags.length > 0 ? (
+
+ {task.tags.slice(0, 3).map((tagValue) => {
+ const tag = availableTags.find((tag) => tag.value === tagValue);
+ if (!tag) return null;
+ return (
+
+ {tag.label}
+
+ );
+ })}
+ {task.tags.length > 3 && (
+
+ +{task.tags.length - 3}
+
+ )}
+
+ ) : (
+ To Be Determined
+ )}
+
+ {formatDate(task.startDate)}
+ {formatDate(task.endDate)}
+
+ {column.title}
+
+
+ ))}
+ >
+ ) : (
+
+
+ No tasks in this column
+
+
+ )}
+
+
+ )}
+
+ );
+ })}
+
+ );
+}
diff --git a/frontend/src/components/ui/task-components/TagsCombobox.tsx b/frontend/src/components/ui/task-components/TagsCombobox.tsx
new file mode 100644
index 0000000..17d52fb
--- /dev/null
+++ b/frontend/src/components/ui/task-components/TagsCombobox.tsx
@@ -0,0 +1,106 @@
+// Allows users to select Labels for Tasks
+
+import { Check, ChevronsUpDown, X } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+} from "@/components/ui/command";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { Badge } from "@/components/ui/badge";
+import { availableTags } from "@/lib/dummyData/dummyTasks";
+import { useState } from "react";
+
+interface TagsComboboxProps {
+ selectedTags: string[];
+ onTagsChange: (tags: string[]) => void;
+}
+
+export function TagsCombobox({ selectedTags, onTagsChange }: TagsComboboxProps) {
+ const [open, setOpen] = useState(false);
+
+ const toggleTag = (tagValue: string) => {
+ if (selectedTags.includes(tagValue)) {
+ onTagsChange(selectedTags.filter((tag) => tag !== tagValue));
+ } else {
+ onTagsChange([...selectedTags, tagValue]);
+ }
+ };
+
+ const removeTag = (tagValue: string) => {
+ onTagsChange(selectedTags.filter((tag) => tag !== tagValue));
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ No tag found.
+
+ {availableTags.map((tag) => (
+ toggleTag(tag.value)}
+ className="cursor-pointer"
+ >
+
+
+
+ ))}
+
+
+
+
+ {selectedTags.length > 0 && (
+
+ {selectedTags.map((tagValue) => {
+ const tag = availableTags.find((tag) => tag.value === tagValue);
+ if (!tag) return null;
+ return (
+
+ {tag.label}
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
diff --git a/frontend/src/components/ui/task-components/TaskCard.tsx b/frontend/src/components/ui/task-components/TaskCard.tsx
new file mode 100644
index 0000000..3c174fd
--- /dev/null
+++ b/frontend/src/components/ui/task-components/TaskCard.tsx
@@ -0,0 +1,133 @@
+// Component for Task Cards
+// Contains the Drag and Drop behavior for the Tasks
+// Handles task creation and editing inside each column
+// Allows for deleting and editing column creation
+
+import type { Task, Id, Column } from "./KanbanBoard";
+import { Ellipsis, Trash, Pencil } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Card } from "@/components/ui/card";
+import { useState } from "react";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { useSortable } from "@dnd-kit/sortable";
+import { CSS } from "@dnd-kit/utilities";
+import { Avatar, AvatarFallback } from "@/components/ui/avatar";
+import { TaskSheet } from "./TaskSheet";
+import { teamMembers, priorityStyles } from "@/lib/dummyData/dummyTasks";
+
+interface Props {
+ column: Column;
+ task: Task;
+ deleteTask: (id: Id) => void;
+ setEditingTask: (task: Task | null) => void;
+}
+
+export function TaskCard({ column, task, deleteTask, setEditingTask }: Props) {
+ const [isSheetOpen, setIsSheetOpen] = useState(false);
+
+ const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
+ id: task.id,
+ data: {
+ type: "Task",
+ task,
+ },
+ disabled: isSheetOpen,
+ });
+
+ const style = {
+ transition,
+ transform: CSS.Transform.toString(transform),
+ };
+
+ const cardClassName = isDragging
+ ? "relative min-h-[60px] p-2 opacity-40 ring-2 ring-blue-300 ring-inset"
+ : "relative min-h-[60px] p-2 cursor-grab active:cursor-grabbing";
+
+ return (
+
+
+ {task.priority && (
+
+ )}
+
+
+
+
+ {!isDragging && (
+
+
+
+
+
+ Task
+
+ {
+ setEditingTask({ ...task });
+ }}
+ className="cursor-pointer"
+ >
+
+ Edit
+
+ {
+ deleteTask(task.id);
+ }}
+ className="cursor-pointer"
+ >
+
+ Delete
+
+
+
+ )}
+ {task.assignees && task.assignees.length > 0 && (
+
+ {task.assignees.slice(0, 3).map((assigneeValue) => {
+ const member = teamMembers.find((member) => member.value === assigneeValue);
+ if (!member) return null;
+ return (
+
+
+ {member.initials}
+
+
+ );
+ })}
+ {task.assignees.length > 3 && (
+
+
+ +{task.assignees.length - 3}
+
+
+ )}
+
+ )}
+
+
+
+ );
+}
diff --git a/frontend/src/components/ui/task-components/TaskSheet.tsx b/frontend/src/components/ui/task-components/TaskSheet.tsx
new file mode 100644
index 0000000..01c2829
--- /dev/null
+++ b/frontend/src/components/ui/task-components/TaskSheet.tsx
@@ -0,0 +1,170 @@
+// Task Sheet component that allows for the display of task information
+
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
+import type { Column, Task } from "./KanbanBoard";
+import { Badge } from "../badge";
+import { Avatar, AvatarFallback } from "../avatar";
+import { Separator } from "@/components/ui/separator";
+import { Label } from "@/components/ui/label";
+import { teamMembers, availableTags } from "@/lib/dummyData/dummyTasks";
+import { PaperclipIcon } from "lucide-react";
+
+interface Props {
+ task: Task;
+ column: Column;
+ truncate?: boolean;
+ onOpenChange?: (open: boolean) => void;
+}
+
+export function TaskSheet({ task, column, truncate, onOpenChange }: Props) {
+ return (
+
+
+
+
+
+ {task.content}
+
+
+
+
+
+
+
+ {task.content}
+
+
+
+
+
+
+
+
+ {task.taskDescription}
+
+
+
+
+ {task.attachments && task.attachments.length > 0 ? (
+
+ {task.attachments.map((file, index) => {
+ if (!(file instanceof File)) {
+ return (
+
+ );
+ }
+ return (
+
+
+ {file.name}
+
+ );
+ })}
+
+ ) : (
+
No attachments
+ )}
+
+
+
+
+
+ {task.assignees && task.assignees.length > 0 ? (
+
+ {task.assignees.slice(0, 3).map((assigneeValue) => {
+ const member = teamMembers.find((member) => member.value === assigneeValue);
+ if (!member) return null;
+ return (
+
+
+ {member.initials}
+
+
+ );
+ })}
+ {task.assignees.length > 3 && (
+
+
+ +{task.assignees.length - 3}
+
+
+ )}
+
+ ) : (
+
To Be Determined
+ )}
+
+
+
+ {task.tags && task.tags.length > 0 ? (
+
+ {task.tags.slice(0, 3).map((tagValue) => {
+ const tag = availableTags.find((tag) => tag.value === tagValue);
+ if (!tag) return null;
+ return (
+
+ {tag.label}
+
+ );
+ })}
+ {task.tags.length > 3 && (
+
+ +{task.tags.length - 3}
+
+ )}
+
+ ) : (
+
To Be Determined
+ )}
+
+
+
+ {column.title}
+
+
+
+ {task.startDate ? (
+
+
{new Date(task.startDate).toLocaleDateString()}
+
+ ) : (
+
To Be Determined
+ )}
+
+
+
+ {task.endDate ? (
+
+
{new Date(task.endDate).toLocaleDateString()}
+
+ ) : (
+
To Be Determined
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/lib/dummyData/dummyTasks.ts b/frontend/src/lib/dummyData/dummyTasks.ts
new file mode 100644
index 0000000..7a56e5e
--- /dev/null
+++ b/frontend/src/lib/dummyData/dummyTasks.ts
@@ -0,0 +1,167 @@
+export const teamMembers = [
+ { value: "ricky", label: "Ricky", initials: "R", color: "bg-blue-500" },
+ { value: "ahan", label: "Ahan", initials: "A", color: "bg-purple-500" },
+ { value: "rachael", label: "Rachael Bergeron", initials: "RB", color: "bg-green-500" },
+ { value: "trinh", label: "Trinh", initials: "T", color: "bg-pink-500" },
+ { value: "cardin", label: "Cardin", initials: "C", color: "bg-orange-500" },
+];
+
+export const availableTags = [
+ { value: "frontend", label: "Frontend", color: "bg-blue-500" },
+ { value: "backend", label: "Backend", color: "bg-green-500" },
+ { value: "database", label: "Database", color: "bg-purple-500" },
+];
+
+export const priorityLevels: Array<{ value: "High" | "Medium" | "Low"; color: string }> = [
+ { value: "High", color: "bg-red-500" },
+ { value: "Medium", color: "bg-yellow-500" },
+ { value: "Low", color: "bg-blue-400" },
+];
+
+export const priorityStyles = {
+ High: "bg-red-500 hover:bg-red-600 text-white",
+ Medium: "bg-yellow-500 hover:bg-yellow-600 text-white",
+ Low: "bg-blue-400 hover:bg-blue-500 text-white",
+};
+
+export const dummyColumns = [
+ {
+ id: 1763765681713,
+ color: "bg-gray-600",
+ colorIndex: 0,
+ description: "This item hasn't been started",
+ title: "Backlog",
+ },
+ {
+ id: 1763765683697,
+ color: "bg-blue-400",
+ colorIndex: 1,
+ description: "This is actively being worked on",
+ title: "In Progress",
+ },
+ {
+ id: 1763765685587,
+ color: "bg-green-500",
+ colorIndex: 2,
+ description: "This is ready to be picked up",
+ title: "In Review",
+ },
+ {
+ id: 1763765687636,
+ color: "bg-yellow-500",
+ colorIndex: 3,
+ description: "This has been completed",
+ title: "Done",
+ },
+];
+
+export const dummyTasks = [
+ {
+ assignees: ["cardin"],
+ attachments: [],
+ columnId: 1763765681713,
+ content: "Testing 1",
+ endDate: new Date(),
+ id: 1763765692316,
+ priority: "High" as const,
+ startDate: new Date(),
+ tags: ["frontend"],
+ taskDescription: "Testing",
+ },
+ {
+ assignees: ["ricky"],
+ attachments: [],
+ columnId: 1763765681713,
+ content: "Testing 2",
+ endDate: new Date(),
+ id: 1763765694622,
+ priority: "Medium" as const,
+ startDate: new Date(),
+ tags: ["backend"],
+ taskDescription: "Testing",
+ },
+ {
+ assignees: ["trinh"],
+ attachments: [],
+ columnId: 1763765681713,
+ content: "Testing 3",
+ endDate: new Date(),
+ id: 1763765697006,
+ priority: "Low" as const,
+ startDate: new Date(),
+ tags: ["frontend"],
+ taskDescription: "Testing",
+ },
+ {
+ assignees: ["ahan"],
+ attachments: [],
+ columnId: 1763765681713,
+ content: "Testing 4",
+ endDate: new Date(),
+ id: 1763765699390,
+ priority: "Medium" as const,
+ startDate: new Date(),
+ tags: ["database"],
+ taskDescription: "Testing",
+ },
+ {
+ assignees: ["rachael"],
+ attachments: [],
+ columnId: 1763765681713,
+ content: "Testing 5",
+ endDate: new Date(),
+ id: 1763765702081,
+ priority: "Low" as const,
+ startDate: new Date(),
+ tags: ["frontend"],
+ taskDescription: "Testing",
+ },
+ {
+ assignees: ["cardin", "rachael", "ahan", "trinh", "ricky"],
+ attachments: [],
+ columnId: 1763765681713,
+ content: "Testing 6",
+ endDate: new Date(),
+ id: 1763765704367,
+ priority: "High" as const,
+ startDate: new Date(),
+ tags: ["database"],
+ taskDescription: "Testing",
+ },
+ {
+ assignees: ["cardin", "rachael", "ahan", "trinh", "ricky"],
+ attachments: [],
+ columnId: 1763765683697,
+ content: "Testing 7",
+ endDate: new Date(),
+ id: 1763765706430,
+ priority: "Medium" as const,
+ startDate: new Date(),
+ tags: ["backend"],
+ taskDescription: "Testing",
+ },
+ {
+ assignees: ["cardin", "rachael", "ahan", "trinh", "ricky"],
+ attachments: [],
+ columnId: 1763765685587,
+ content: "Testing 8",
+ endDate: new Date(),
+ id: 1763765708602,
+ priority: "Low" as const,
+ startDate: new Date(),
+ tags: ["frontend"],
+ taskDescription: "Testing",
+ },
+ {
+ assignees: ["cardin", "rachael", "ahan", "trinh", "ricky"],
+ attachments: [],
+ columnId: 1763765687636,
+ content: "Testing 9",
+ endDate: new Date(),
+ id: 1763765710925,
+ priority: "Medium" as const,
+ startDate: new Date(),
+ tags: ["frontend"],
+ taskDescription: "Testing",
+ },
+];
diff --git a/frontend/src/pages/Kanban.tsx b/frontend/src/pages/Kanban.tsx
new file mode 100644
index 0000000..c5356c5
--- /dev/null
+++ b/frontend/src/pages/Kanban.tsx
@@ -0,0 +1,257 @@
+// Root file that handles all of the Tasks and Columns
+// Includes states for information that needs to be passed between MyTask Tab components and Overall Tab components
+// Filtering states that are also passed down
+
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { LearnMore } from "@/components/ui/task-components/LearnMore";
+import { MyTask } from "@/components/ui/task-components/MyTaskContent";
+import { KanbanBoard } from "@/components/ui/task-components/KanbanBoard";
+import type { Task, Column, Id } from "@/components/ui/task-components/KanbanBoard";
+import { useState, useEffect } from "react";
+import { TaskTable } from "@/components/ui/task-components/MyTaskTable";
+import { FilterCommandBar } from "@/components/ui/task-components/Filter";
+import { dummyColumns, dummyTasks } from "@/lib/dummyData/dummyTasks";
+
+export default function MemberTabs() {
+ const [selectedColumnId, setSelectedColumnId] = useState(null);
+ const [selectedAssignee, setSelectedAssignee] = useState(null);
+ const [selectedPriority, setSelectedPriority] = useState<"High" | "Medium" | "Low" | null>(null);
+ const [selectedStartDate, setSelectedStartDate] = useState(null);
+ const [selectedEndDate, setSelectedEndDate] = useState(null);
+ const [selectedLabel, setSelectedLabel] = useState(null);
+ const [overallSelectedColumns, setOverallSelectedColumns] = useState([]);
+ const [overallSelectedAssignees, setOverallSelectedAssignees] = useState([]);
+ const [overallSelectedPriority, setOverallSelectedPriority] = useState([]);
+ const [overallSelectedStartDates, setOverallSelectedStartDates] = useState([]);
+ const [overallSelectedEndDates, setOverallSelectedEndDates] = useState([]);
+ const [overallSelectedLabels, setOverallSelectedLabels] = useState([]);
+ const [activeTab, setActiveTab] = useState("my-task");
+ const [myTaskFilters, setMyTaskFilters] = useState({
+ statuses: [] as string[],
+ assignees: [] as string[],
+ priorities: [] as string[],
+ labels: [] as string[],
+ startDates: [] as string[],
+ endDates: [] as string[],
+ });
+
+ const [tasks, setTasks] = useState(() => {
+ const savedData = localStorage.getItem("myTaskData");
+ return savedData ? JSON.parse(savedData) : dummyTasks;
+ });
+
+ const [columns, setColumns] = useState(() => {
+ const savedData = localStorage.getItem("myColumnData");
+ return savedData ? JSON.parse(savedData) : dummyColumns;
+ });
+
+ useEffect(() => {
+ localStorage.setItem("myTaskData", JSON.stringify(tasks));
+ }, [tasks]);
+
+ useEffect(() => {
+ localStorage.setItem("myColumnData", JSON.stringify(columns));
+ }, [columns]);
+
+ useEffect(() => {
+ if (activeTab === "my-task") {
+ setSelectedColumnId(null);
+ setSelectedAssignee(null);
+ setSelectedPriority(null);
+ setSelectedStartDate(null);
+ setSelectedEndDate(null);
+ setSelectedLabel(null);
+ } else {
+ setOverallSelectedColumns([]);
+ setOverallSelectedAssignees([]);
+ setOverallSelectedPriority([]);
+ setOverallSelectedStartDates([]);
+ setOverallSelectedEndDates([]);
+ setOverallSelectedLabels([]);
+ }
+ }, [activeTab]);
+
+ const filteredTasks = tasks.filter((task) => {
+ if (
+ myTaskFilters.statuses.length > 0 &&
+ !myTaskFilters.statuses.includes(String(task.columnId))
+ )
+ return false;
+
+ if (
+ myTaskFilters.assignees.length > 0 &&
+ !task.assignees?.some((a) => myTaskFilters.assignees.includes(a))
+ )
+ return false;
+
+ if (
+ myTaskFilters.priorities.length > 0 &&
+ !myTaskFilters.priorities.includes(task.priority ?? "")
+ )
+ return false;
+
+ if (myTaskFilters.startDates.length > 0) {
+ const formatted = task.startDate ? new Date(task.startDate).toLocaleDateString("en-US") : "";
+ if (!myTaskFilters.startDates.includes(formatted)) return false;
+ }
+
+ if (myTaskFilters.endDates.length > 0) {
+ const formatted = task.endDate ? new Date(task.endDate).toLocaleDateString("en-US") : "";
+ if (!myTaskFilters.endDates.includes(formatted)) return false;
+ }
+
+ if (
+ myTaskFilters.labels.length > 0 &&
+ !task.tags?.some((tag) => myTaskFilters.labels.includes(tag))
+ )
+ return false;
+
+ return true;
+ });
+
+ const overallFiltered = tasks.filter((task) => {
+ if (overallSelectedColumns.length > 0 && !overallSelectedColumns.includes(task.columnId)) {
+ return false;
+ }
+
+ if (
+ overallSelectedAssignees.length > 0 &&
+ !task.assignees?.some((assignee) => overallSelectedAssignees.includes(assignee))
+ ) {
+ return false;
+ }
+
+ if (
+ overallSelectedPriority.length > 0 &&
+ !overallSelectedPriority.includes(task.priority ?? "")
+ ) {
+ return false;
+ }
+
+ if (overallSelectedStartDates.length > 0) {
+ const formatted = task.startDate ? new Date(task.startDate).toLocaleDateString("en-US") : "";
+ if (!overallSelectedStartDates.includes(formatted)) return false;
+ }
+
+ if (overallSelectedEndDates.length > 0) {
+ const formatted = task.endDate ? new Date(task.endDate).toLocaleDateString("en-US") : "";
+ if (!overallSelectedEndDates.includes(formatted)) return false;
+ }
+
+ if (overallSelectedLabels.length > 0) {
+ if (!task.tags?.some((tag) => overallSelectedLabels.includes(tag))) return false;
+ }
+
+ return true;
+ });
+
+ return (
+
+
+
+
+
+ My Task
+
+
+ Overall
+
+
+
+
+
+ {
+ setMyTaskFilters((prev) => ({
+ ...prev,
+ [key]: next,
+ }));
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
{
+ if (key === "statuses") setOverallSelectedColumns(next.map(Number));
+ if (key === "assignees") setOverallSelectedAssignees(next);
+ if (key === "priorities") setOverallSelectedPriority(next);
+ if (key === "labels") setOverallSelectedLabels(next);
+ if (key === "startDates") setOverallSelectedStartDates(next);
+ if (key === "endDates") setOverallSelectedEndDates(next);
+ }}
+ />
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx
index 74b0a06..9f3ac69 100644
--- a/frontend/src/routes.tsx
+++ b/frontend/src/routes.tsx
@@ -9,6 +9,7 @@ const Admin = lazy(() => import("./pages/Admin.tsx"));
const Dashboard = lazy(() => import("./pages/Dashboard.tsx"));
const Orders = lazy(() => import("./pages/Orders.tsx"));
const Projects = lazy(() => import("./pages/Projects.tsx"));
+const Tasks = lazy(() => import("./pages/Kanban.tsx"));
export const AppRoutes = () => {
return (
@@ -23,6 +24,7 @@ export const AppRoutes = () => {
} />
} />
} />
+ } />
{/* 404 Redirect */}
} />