nicholas/storage-manager#1: use tailwind; implement edit, delete, create boxes and items

This commit is contained in:
2025-04-09 19:22:54 -05:00
parent d00615b197
commit b031ce4fde
23 changed files with 971 additions and 613 deletions
+1 -1
View File
@@ -9,7 +9,7 @@
<title>Storage Manager</title>
</head>
<body class="scroll-container">
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
+509 -57
View File
@@ -1,17 +1,19 @@
{
"name": "storage-manager-client",
"name": "client",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "storage-manager-client",
"name": "client",
"version": "0.0.0",
"dependencies": {
"@tailwindcss/vite": "^4.1.3",
"axios": "^1.8.4",
"color": "^5.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"tailwindcss": "^4.1.3"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
@@ -328,7 +330,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -345,7 +346,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -362,7 +362,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -379,7 +378,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -396,7 +394,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -413,7 +410,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -430,7 +426,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -447,7 +442,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -464,7 +458,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -481,7 +474,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -498,7 +490,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -515,7 +506,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -532,7 +522,6 @@
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -549,7 +538,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -566,7 +554,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -583,7 +570,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -600,7 +586,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -617,7 +602,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -634,7 +618,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -651,7 +634,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -668,7 +650,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -685,7 +666,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -702,7 +682,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -719,7 +698,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -736,7 +714,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1036,7 +1013,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1050,7 +1026,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1064,7 +1039,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1078,7 +1052,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1092,7 +1065,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1106,7 +1078,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1120,7 +1091,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1134,7 +1104,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1148,7 +1117,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1162,7 +1130,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1176,7 +1143,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1190,7 +1156,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1204,7 +1169,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1218,7 +1182,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1232,7 +1195,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1246,7 +1208,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1260,7 +1221,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1274,7 +1234,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1288,7 +1247,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1302,13 +1260,236 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@tailwindcss/node": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.3.tgz",
"integrity": "sha512-H/6r6IPFJkCfBJZ2dKZiPJ7Ueb2wbL592+9bQEl2r73qbX6yGnmQVIfiUvDRB2YI0a3PWDrzUwkvQx1XW1bNkA==",
"license": "MIT",
"dependencies": {
"enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2",
"lightningcss": "1.29.2",
"tailwindcss": "4.1.3"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.3.tgz",
"integrity": "sha512-t16lpHCU7LBxDe/8dCj9ntyNpXaSTAgxWm1u2XQP5NiIu4KGSyrDJJRlK9hJ4U9yJxx0UKCVI67MJWFNll5mOQ==",
"license": "MIT",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.3",
"@tailwindcss/oxide-darwin-arm64": "4.1.3",
"@tailwindcss/oxide-darwin-x64": "4.1.3",
"@tailwindcss/oxide-freebsd-x64": "4.1.3",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.3",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.3",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.3",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.3",
"@tailwindcss/oxide-linux-x64-musl": "4.1.3",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.3",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.3"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.3.tgz",
"integrity": "sha512-cxklKjtNLwFl3mDYw4XpEfBY+G8ssSg9ADL4Wm6//5woi3XGqlxFsnV5Zb6v07dxw1NvEX2uoqsxO/zWQsgR+g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.3.tgz",
"integrity": "sha512-mqkf2tLR5VCrjBvuRDwzKNShRu99gCAVMkVsaEOFvv6cCjlEKXRecPu9DEnxp6STk5z+Vlbh1M5zY3nQCXMXhw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.3.tgz",
"integrity": "sha512-7sGraGaWzXvCLyxrc7d+CCpUN3fYnkkcso3rCzwUmo/LteAl2ZGCDlGvDD8Y/1D3ngxT8KgDj1DSwOnNewKhmg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.3.tgz",
"integrity": "sha512-E2+PbcbzIReaAYZe997wb9rId246yDkCwAakllAWSGqe6VTg9hHle67hfH6ExjpV2LSK/siRzBUs5wVff3RW9w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.3.tgz",
"integrity": "sha512-GvfbJ8wjSSjbLFFE3UYz4Eh8i4L6GiEYqCtA8j2Zd2oXriPuom/Ah/64pg/szWycQpzRnbDiJozoxFU2oJZyfg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.3.tgz",
"integrity": "sha512-35UkuCWQTeG9BHcBQXndDOrpsnt3Pj9NVIB4CgNiKmpG8GnCNXeMczkUpOoqcOhO6Cc/mM2W7kaQ/MTEENDDXg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.3.tgz",
"integrity": "sha512-dm18aQiML5QCj9DQo7wMbt1Z2tl3Giht54uVR87a84X8qRtuXxUqnKQkRDK5B4bCOmcZ580lF9YcoMkbDYTXHQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.3.tgz",
"integrity": "sha512-LMdTmGe/NPtGOaOfV2HuO7w07jI3cflPrVq5CXl+2O93DCewADK0uW1ORNAcfu2YxDUS035eY2W38TxrsqngxA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.3.tgz",
"integrity": "sha512-aalNWwIi54bbFEizwl1/XpmdDrOaCjRFQRgtbv9slWjmNPuJJTIKPHf5/XXDARc9CneW9FkSTqTbyvNecYAEGw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.3.tgz",
"integrity": "sha512-PEj7XR4OGTGoboTIAdXicKuWl4EQIjKHKuR+bFy9oYN7CFZo0eu74+70O4XuERX4yjqVZGAkCdglBODlgqcCXg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.3.tgz",
"integrity": "sha512-T8gfxECWDBENotpw3HR9SmNiHC9AOJdxs+woasRZ8Q/J4VHN0OMs7F+4yVNZ9EVN26Wv6mZbK0jv7eHYuLJLwA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.3.tgz",
"integrity": "sha512-lUI/QaDxLtlV52Lho6pu07CG9pSnRYLOPmKGIQjyHdTBagemc6HmgZxyjGAQ/5HMPrNeWBfTVIpQl0/jLXvWHQ==",
"license": "MIT",
"dependencies": {
"@tailwindcss/node": "4.1.3",
"@tailwindcss/oxide": "4.1.3",
"tailwindcss": "4.1.3"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1358,7 +1539,6 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
@@ -1757,6 +1937,15 @@
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -1778,6 +1967,19 @@
"dev": true,
"license": "ISC"
},
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -1827,7 +2029,6 @@
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -2176,7 +2377,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -2281,6 +2481,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -2397,6 +2603,15 @@
"dev": true,
"license": "ISC"
},
"node_modules/jiti": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -2488,6 +2703,234 @@
"node": ">= 0.8.0"
}
},
"node_modules/lightningcss": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
"integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==",
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-darwin-arm64": "1.29.2",
"lightningcss-darwin-x64": "1.29.2",
"lightningcss-freebsd-x64": "1.29.2",
"lightningcss-linux-arm-gnueabihf": "1.29.2",
"lightningcss-linux-arm64-gnu": "1.29.2",
"lightningcss-linux-arm64-musl": "1.29.2",
"lightningcss-linux-x64-gnu": "1.29.2",
"lightningcss-linux-x64-musl": "1.29.2",
"lightningcss-win32-arm64-msvc": "1.29.2",
"lightningcss-win32-x64-msvc": "1.29.2"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz",
"integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz",
"integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz",
"integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz",
"integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==",
"cpu": [
"arm"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz",
"integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz",
"integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz",
"integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz",
"integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz",
"integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz",
"integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -2575,7 +3018,6 @@
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
@@ -2691,14 +3133,12 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -2794,7 +3234,6 @@
"version": "4.39.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.39.0.tgz",
"integrity": "sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.7"
@@ -2873,7 +3312,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -2905,6 +3343,21 @@
"node": ">=8"
}
},
"node_modules/tailwindcss": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.3.tgz",
"integrity": "sha512-2Q+rw9vy1WFXu5cIxlvsabCwhU2qUwodGq03ODhLJ0jW4ek5BUtoCsnLB0qG+m8AHgEsSJcJGDSDe06FXlP74g==",
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -2963,7 +3416,6 @@
"version": "6.2.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz",
"integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
+3 -1
View File
@@ -10,10 +10,12 @@
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.3",
"axios": "^1.8.4",
"color": "^5.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"tailwindcss": "^4.1.3"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
+3 -3
View File
@@ -17,7 +17,7 @@ export const api = axios.create({
api.interceptors.response.use(
response => response,
error => {
console.error('API error:', error);
return Promise.reject(error);
console.error('API error:', error);
return Promise.reject(error);
}
);
);
+69 -35
View File
@@ -1,33 +1,43 @@
import { useEffect, useState } from 'react';
import { getAllBoxes, Box, BoxCard, BoxFormModal } from './features/box';
import './styles/app.css';
import './styles/button.css';
import './styles/inputBox.css';
import './styles/textArea.css';
import './index.css';
const App = () => {
const [boxes, setBoxes] = useState<Box[]>([]);
const [isModalOpen, setModalOpen] = useState(false);
const handleCreateBox = (newBox: Box) => {
setBoxes((boxes) => [...boxes, newBox]);
};
const handleDeleteBox = (id: string) => {
setBoxes((boxes) => boxes.filter(box => box._id !== id));
}
const toggleModal = () => {
setModalOpen(!isModalOpen);
const [isBoxFormModalOpen, setBoxFormModalOpen] = useState(false);
const handleBoxFormModalOpen = () => {
setBoxFormModalOpen(true);
};
const handleBoxFormModalClose = () => {
setBoxFormModalOpen(false);
};
const handleSubmit = () => {
setModalOpen(false);
// box filter
const [searchString, setSearch] = useState('');
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearch(event.target.value);
};
const filteredBoxes = boxes.filter(box => {
const ss = searchString.toLowerCase();
const handleClose = () => {
setModalOpen(false);
};
const matchingItems = box.items?.filter(item =>
item.name.toLowerCase().includes(ss) ||
item.description.toLowerCase().includes(ss)
);
return (
box.name.toLowerCase().includes(ss) ||
box.description.toLowerCase().includes(ss) ||
matchingItems && matchingItems.length > 0
);
});
useEffect(() => {
const fetchData = async () => {
const response = await getAllBoxes();
@@ -44,25 +54,49 @@ const App = () => {
}, []);
return (
<div>
<BoxFormModal isOpen={isModalOpen} onCreateBox={handleCreateBox} onClose={handleClose} />
<div className="filter-container">
<span className='material-icons icon'>search</span>
<input spellCheck='false' className='filter' type='search' id='box-filter' name='filter' />
<span className='material-icons icon clear'>clear</span>
</div>
<div className='header-container'>
<button className='button' onClick={toggleModal}>new box</button>
</div>
<div className='body-container scroll-container'>
<ul className='list-box'>
{boxes.map((item) => (
<BoxCard key={item._id} box={item} />
))}
</ul>
</div>
</div>
);
<>
<BoxFormModal
mode="create"
isOpen={isBoxFormModalOpen}
onSubmitBox={handleCreateBox}
onClose={handleBoxFormModalClose}
/>
<div className="">
<label htmlFor="box-filter" className="text-sm font-medium text-gray-700">Search</label>
<input
onChange={handleSearchChange}
spellCheck="false"
type="search"
id="box-filter"
name="filter"
className=""
/>
</div>
<div className="">
<button
onClick={handleBoxFormModalOpen}
className=""
>
New Box
</button>
</div>
<div className='box-card-list-container'>
<ul className='box-card-list'>
{filteredBoxes.map((item) => (
<BoxCard
key={item._id}
initialBox={item}
onDelete={handleDeleteBox}
/>
))}
</ul>
</div>
</>
);
};
export default App;
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

+1 -1
View File
@@ -8,7 +8,7 @@ const ColorSelect = ({ colorData, handler, boxColor }) => {
}
return (
<label onMouseEnter={() => handleHover(true)} onMouseLeave={() => handleHover(false)} style={style} className={`color ${boxColor === colorData.title ? 'selected' : ''}`}>
<label onMouseEnter={() => handleHover(true)} onMouseLeave={() => handleHover(false)} style={style} >
<input checked={boxColor === colorData.title} onChange={handler} type='radio' value={colorData.title}></input>
</label>
)
+1 -1
View File
@@ -4,5 +4,5 @@ export interface Box {
_id?: string;
name: string;
description: string;
items: Item[];
items?: Item[];
}
+93 -26
View File
@@ -1,51 +1,118 @@
import { useState } from 'react';
import { Box } from '../';
import { useState, useEffect, useRef } from 'react';
import { Box, BoxFormModal, deleteBox } from '../';
import { Item, ItemCard, ItemFormModal } from '../../item';
interface BoxCardProps {
box: Box;
initialBox: Box;
onDelete: (id: string) => void;
}
export const BoxCard: React.FC<BoxCardProps> = ({ box }) => {
export const BoxCard: React.FC<BoxCardProps> = ({ initialBox, onDelete }) => {
const [items, setItems] = useState<Item[]>(box.items);
const [modalOpen, setModalOpen] = useState(false);
const [items, setItems] = useState<Item[]>(initialBox.items!);
const [box, setBox] = useState<Box>(initialBox);
const [isBoxFormModalOpen, setBoxFormModalOpen] = useState(false);
const [isItemFormModalOpen, setItemFormModalOpen] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const menuButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
menuRef.current && !menuRef.current.contains(event.target as Node) &&
menuButtonRef.current && !menuButtonRef.current.contains(event.target as Node)
) {
setIsMenuOpen(false);
}
};
document.addEventListener('click', handleClickOutside);
return () => {
document.removeEventListener('click', handleClickOutside);
};
}, []);
const toggleMenu = () => {
setIsMenuOpen(prevState => !prevState);
};
const handleOpenItemFormModal = () => {
setModalOpen(true);
setItemFormModalOpen(true);
}
const handleClose = () => {
setModalOpen(false);
const handleCloseItemFormModal = () => {
setItemFormModalOpen(false);
};
const handleOpenBoxFormModal = () => {
setBoxFormModalOpen(true);
}
const handleCloseBoxFormModal = () => {
setBoxFormModalOpen(false);
};
const handleCreateItem = (newItem: Item) => {
setItems((items) => [...items, newItem]);
};
const handleClickEditBox = () => {
setBoxFormModalOpen(true);
}
const handleUpdateBox = (updatedBox: Box) => {
setBox(updatedBox);
}
const handleDeleteBox = async () => {
if (box._id != null) {
const response = await deleteBox(box._id);
if (response.success) {
onDelete(box._id);
} else {
console.error('No data returned in the response');
}
}
}
const handleDeleteItem = (id: string) => {
setItems((items) => items.filter(item => item._id !== id));
}
return (
<li className="box">
<ItemFormModal isOpen={modalOpen} onCreateItem={handleCreateItem} onClose={handleClose} boxId={box._id} />
<div className='menu-more-container'></div>
<div className="box-header">
<div className="box-title-container">
<h2 className="box-title">{box.name}</h2>
<h3 className="box-subtitle">{box.description}</h3>
<>
<BoxFormModal mode="edit" initialData={box} isOpen={isBoxFormModalOpen} onSubmitBox={handleUpdateBox} onClose={handleCloseBoxFormModal} />
<ItemFormModal mode="create" isOpen={isItemFormModalOpen} onSubmitItem={handleCreateItem} onClose={handleCloseItemFormModal} boxId={box._id!} />
<li className='box-card'>
<div>
<div>
<h2 className='box-card-title'>{box.name}</h2>
<h3 className='box-card-description'>{box.description}</h3>
</div>
<span className='menu-btn box material-icons'>more_vert</span>
<button ref={menuButtonRef} onClick={toggleMenu}>menu</button>
{isMenuOpen && (
<div ref={menuRef}>
<ul>
<li><button onClick={handleClickEditBox}>edit</button></li>
<li><button onClick={handleDeleteBox}>delete</button></li>
</ul>
</div>
)}
</div>
<div className="head-divider" />
<ul className={`list-box-item scroll-container ${box.items?.length === 0 ? 'empty' : ''}`}>
<div className='item-card-list-container'>
<ul className='item-card-list'>
{items?.map((item) => (
<ItemCard key={item._id} item={item}></ItemCard>
<ItemCard key={item._id} boxId={box._id!} initialItem={item} onDelete={handleDeleteItem} />
))}
</ul>
<div className="menu-bottom">
<span
className="material-icons add"
onClick={handleOpenItemFormModal}>
add
</span>
</div>
<div>
<button onClick={handleOpenItemFormModal}>add</button>
</div>
</li>
</>
);
};
@@ -1,42 +1,40 @@
import { useState } from 'react';
import Modal from '../../../modal';
import { Box, createBox } from '..';
import { Box, createBox, updateBox } from '..';
import { Item } from '../../item';
interface BoxFormModalProps {
mode: string;
isOpen: boolean;
onClose: () => void;
onCreateBox: (box: Box) => void;
onSubmitBox: (box: Box) => void;
initialData?: Partial<Box>;
}
export const BoxFormModal: React.FC<BoxFormModalProps> = ({ isOpen, onClose, onCreateBox, initialData }) => {
export const BoxFormModal: React.FC<BoxFormModalProps> = ({ mode, initialData, isOpen, onClose, onSubmitBox }) => {
const [boxName, setBoxName] = useState(initialData?.name ?? '');
const [boxDescription, setBoxDescription] = useState(initialData?.description ?? '');
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = event.currentTarget;
const formData = new FormData(form);
const boxData: Box = {
name: formData.get('name') as string,
description: formData.get('description') as string,
items: initialData?.items as Item[]
name: boxName as string,
description: boxDescription as string
};
const isEdit = mode === 'edit';
try {
const response = await createBox(boxData);
if (response.success) {
if (response.data != null) {
onCreateBox(response.data);
} else {
console.error('No data returned in the response');
}
} else {
console.log(response.error || 'Unknown error');
const response = isEdit ? await updateBox(initialData?._id!, boxData) : await createBox(boxData);
if (!response.success || !response.data) {
console.error('Box operation failed:', response.error ?? 'Unknown error');
return;
}
onSubmitBox(response.data);
} catch (err) {
console.error(err);
}
finally {
console.error(`Error during ${isEdit ? 'update' : 'create'}:`, err);
} finally {
onClose();
}
@@ -48,18 +46,16 @@ export const BoxFormModal: React.FC<BoxFormModalProps> = ({ isOpen, onClose, onC
<input value="" id='id' name='id' readOnly={true} hidden={true} />
<h1>Edit Box</h1>
<fieldset>
<label className='input' htmlFor="box-name">name</label>
<input className='input' id='box-name' name='name' type='text' />
<label className='input' htmlFor="box-description">description</label>
<input className='input' id='box-description' name='description' type='text' />
<div className="modal-actions">
<button className='button' type="submit">Create</button>
<button className='button' type="button" onClick={onClose}>Cancel</button>
<label htmlFor="box-name">name</label>
<input value={boxName} onChange={(e) => setBoxName(e.target.value)} id='box-name' name='name' type='text' />
<label htmlFor="box-description">description</label>
<input value={boxDescription} onChange={(e) => setBoxDescription(e.target.value)} id='box-description' name='description' type='text' />
<div>
<button type="submit">Submit</button>
<button type="button" onClick={onClose}>Cancel</button>
</div>
</fieldset>
</form>
</Modal>
);
};
@@ -27,6 +27,38 @@ const createBox = async (boxData: Box): Promise<ApiResponse<Box>> => {
};
}
}
const updateBox = async (boxId: string, boxData: Box): Promise<ApiResponse<Box>> => {
try {
console.log(boxData);
const response = await api.put<Box>(`/boxes/${boxId}`, boxData);
return {
success: true,
data: response.data,
};
} catch (error) {
console.error('Error updating box:', error);
return {
success: false,
error: 'Could not update box',
};
}
}
const deleteBox = async (id: string): Promise<ApiResponse<null>> => {
try {
const response = await api.delete<null>(`boxes/${id}`);
return {
success: true,
}
}
catch (error) {
console.error('Error deleting box:', error);
return {
success: false,
error: 'Could not delete box'
}
}
}
export { getAllBoxes, createBox }
export { getAllBoxes, createBox, updateBox, deleteBox }
@@ -1,20 +1,43 @@
import { Item } from '../item.types';
import { useState } from 'react';
import { Item, ItemFormModal, deleteItem } from '../';
interface ItemCardProps {
item: Item;
boxId: string;
initialItem: Item;
onDelete: (id: string) => void;
}
export const ItemCard: React.FC<ItemCardProps> = ({ item }) => {
export const ItemCard: React.FC<ItemCardProps> = ({ boxId, initialItem, onDelete }) => {
const [isItemFormModalOpen, setIsItemFormModalOpen] = useState(false);
const [item, setItem] = useState(initialItem);
const handleCloseItemFormModal = () => {
setIsItemFormModalOpen(false);
}
const handleClickEditItem = async () => {
setIsItemFormModalOpen(true);
}
const handleUpdateItem = (updatedItem: Item) => {
setItem(updatedItem);
}
const handleDeleteItem = async () => {
if (item._id != null) {
const response = await deleteItem(boxId, item._id);
if (response.success) {
onDelete(item._id);
} else {
console.error('No data returned in the response');
}
}
}
return (
<li className='item'>
<div className='item-menu-more-container'></div>
<span className='item-name'>{item.name}</span>
<span className='menu-btn item material-icons'>
remove_red_eye
</span>
<span className='menu-btn item material-icons'>
more_vert
</span>
<li>
<ItemFormModal mode="edit" isOpen={isItemFormModalOpen} onSubmitItem={handleUpdateItem} onClose={handleCloseItemFormModal} boxId={boxId} initialData={item} />
<span>{item.name}</span>
<button onClick={handleClickEditItem} >edit</button>
<button onClick={handleDeleteItem} >remove</button>
</li>
);
};
@@ -1,38 +1,44 @@
import { useState } from 'react';
import Modal from '../../../modal';
import { Item, createItem } from '..';
import { Item, createItem, updateItem } from '..';
import { createBox } from '../../box';
interface ItemFormModalProps {
mode: string;
isOpen: boolean;
onClose: () => void;
onCreateItem: (item: Item) => void;
boxId?: string;
onSubmitItem: (item: Item) => void;
boxId: string;
initialData?: Partial<Item>;
}
export const ItemFormModal: React.FC<ItemFormModalProps> = ({ isOpen, onClose, onCreateItem, boxId, initialData }) => {
export const ItemFormModal: React.FC<ItemFormModalProps> = ({ mode, initialData, isOpen, onClose, onSubmitItem, boxId }) => {
const [itemName, setItemName] = useState(initialData?.name ?? '');
const [itemDescription, setItemDescription] = useState(initialData?.description ?? '');
const [itemQuantity, setItemQuantity] = useState(initialData?.quantity ?? 0);
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = event.currentTarget;
const formData = new FormData(form);
const isEdit = mode === 'edit';
const itemData: Item = {
name: formData.get('name') as string,
description: formData.get('description') as string,
quantity: parseInt(formData.get('quantity') as string, 10) as number,
name: itemName as string,
description: itemDescription as string,
quantity: itemQuantity as number,
};
try {
const response = await createItem(itemData, boxId);
if (response.success) {
onCreateItem(response.data as Item);
} else {
console.error(response.error);
const response = isEdit ? await updateItem(boxId!,initialData?._id!, itemData) : await createItem(itemData, boxId);
if (!response.success || !response.data) {
console.error('Item operation failed:', response.error ?? 'Unknown error');
return;
}
onSubmitItem(response.data);
} catch (err) {
console.error(err);
}
finally {
console.error(`Error during ${isEdit ? 'update' : 'create'}:`, err);
} finally {
onClose();
}
@@ -45,18 +51,18 @@ export const ItemFormModal: React.FC<ItemFormModalProps> = ({ isOpen, onClose, o
<h1>Edit Item</h1>
<fieldset>
<label className='input' htmlFor="item-name">name</label>
<input className='input' id='item-name' name='name' type='text' />
<label htmlFor="item-name">name</label>
<input value={itemName} onChange={(e) => setItemName(e.currentTarget.value)} id='item-name' name='name' type='text' />
<label className='input' htmlFor="item-description">description</label>
<input className='input' id='item-description' name='description' type='text' />
<label htmlFor="item-description">description</label>
<input value={itemDescription} onChange={(e) => setItemDescription(e.currentTarget.value)}id='item-description' name='description' type='text' />
<label className='input' htmlFor="item-quantity">quantity</label>
<input className='input' id='item-quantity' name='quantity' type='number' />
<label htmlFor="item-quantity">quantity</label>
<input value={itemQuantity} onChange={(e) => setItemQuantity(parseInt(e.currentTarget.value, 10))}id='item-quantity' name='quantity' type='number' />
<div className="modal-actions">
<button className='button' type="submit">Create</button>
<button className='button' type="button" onClick={onClose}>Cancel</button>
<div>
<button type="submit">Submit</button>
<button type="button" onClick={onClose}>Cancel</button>
</div>
</fieldset>
@@ -1,7 +1,7 @@
import { api, ApiResponse } from '../../../api';
import { Item } from '..';
const createItem = async (itemData: Item, boxId?: string,): Promise<ApiResponse<Item | null>> => {
const createItem = async (itemData: Item, boxId: string,): Promise<ApiResponse<Item>> => {
try {
const response = await api.post<Item>(`/boxes/${boxId}/items`, itemData);
return {
@@ -12,10 +12,40 @@ const createItem = async (itemData: Item, boxId?: string,): Promise<ApiResponse<
console.error('Error creating item:', error);
return {
success: false,
data: null,
error: 'Could not create item',
};
}
}
const updateItem = async (boxId: string, itemId: string, itemData: Item): Promise<ApiResponse<Item>> => {
try {
const response = await api.put<Item>(`/boxes/${boxId}/items/${itemId}`, itemData);
return {
success: true,
data: response.data,
};
}
catch (error) {
console.error('Error updating item:', error);
return {
success: false,
error: 'Could not update item',
};
}
}
const deleteItem = async (boxId: string, itemId: string): Promise<ApiResponse<null>> => {
try {
const response = await api.delete<null>(`boxes/${boxId}/items/${itemId}`);
return {
success: true,
}
}
catch (error) {
console.error('Error deleting item:', error);
export { createItem }
return {
success: false,
error: 'Could not delete item'
}
}
}
export { createItem, updateItem, deleteItem }
+22
View File
@@ -1,3 +1,5 @@
@import "tailwindcss";
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
@@ -11,3 +13,23 @@ code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
.box-card-list-container {
@apply flex
}
.box-card-list {
@apply flex flex-wrap space-x-4 p-0 m-0;
}
.box-card {
@apply bg-white p-6 m-2 min-w-sm max-h-128 rounded-lg shadow-md hover:shadow-lg transition-all;
}
.box-card-title {
@apply text-xl font-semibold text-gray-800 mb-2;
}
.box-card-description {
@apply text-sm text-gray-600 mb-4;
}
+3 -4
View File
@@ -1,5 +1,4 @@
import { ReactNode } from 'react';
import './styles/modal.css';
interface ModalProps {
isOpen: boolean;
@@ -10,9 +9,9 @@ const Modal: React.FC<ModalProps> = ({ isOpen, children }) => {
if (!isOpen) return null;
return (
<div className='modal-container'>
<div className='modal-content'>
<div className='modal-body'>
<div>
<div>
<div>
{children}
</div>
</div>
-277
View File
@@ -1,277 +0,0 @@
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
:root {
--black_light: #9aa0a6;
--black_dark: #212121;
--white_light:#fafafa;
--white_darker: #e0e0e0;
}
html, body, #root {
margin: 0;
}
html {
background-color: var(--white_light);
color: var(--black_dark)
}
.body-container {
margin: 0 2em;
overflow:auto;
}
form > fieldset > label {
display: flex;
}
form > fieldset {
border: none;
}
h1, h2, h3, h4 {
font-weight:normal;
margin: 0;
padding: 0;
}
.filter-container {
margin: 1rem 3rem;
display: flex;
align-items: center;
width: 344px;
height: 44px;
background-color: white;
font-size: 1rem;
border-radius: 2rem;
border: 1px solid #dfe1e5;
}
.filter-container > span.icon {
font-size: 1rem;
margin: 0 0.75rem 0 1rem;
color: var(--black_light);
}
.filter-container > span.icon.clear {
margin-left: auto;
}
.filter-container > span.icon.clear:hover {
cursor: pointer;
color: var(--black_dark);
}
.filter {
width: 100%;
font-weight: lighter;
border: none;
height: 100%;
font-size: 1rem;
}
.filter:focus {
outline: none;
}
.filter::-webkit-search-cancel-button {
-webkit-appearance: none;
}
button:hover {
cursor: pointer;
}
.box-header {
display: flex;
align-items: center;
padding: 0 16px;
}
.box-menu-container {
margin-right: 1em;
margin-left: auto;
}
.box-title {
font-size: 1rem;
line-height: 2rem;
}
.box-subtitle {
font-size: 0.875rem;
line-height: 1.25rem;
opacity: 0.6;
text-transform: lowercase;
}
ul.list-box {
padding: 10px;
display: grid;
grid-template-columns: repeat(4, 1fr);
margin: 0;
gap: 1em;
}
li.box {
position: relative;
height: min-content;
width: auto;
padding: 16px 0;
background-color: white;
border-radius: 4px;
border-width: 1px;
border-style: solid;
border-color: var(--white_darker);
box-shadow:
0px 0px 0px 0px rgba(0, 0, 0, 0.2),
0px 0px 0px 0px rgba(0, 0, 0, 0.14),
0px 0px 0px 0px rgba(0,0,0,.12);
list-style: none;
}
li.box.target:hover {
cursor: pointer;
box-shadow:
0 3px 6px rgba(0,0,0,0.16),
0 3px 6px rgba(0,0,0,0.23);
}
.item-menu-more-container {
z-index: 1;
}
.item-menu-more-list {
padding: 0;
width: 120px;
position: absolute;
margin: 0 10px 0 0;
top: 2rem;
right: 0;
z-index: 1;
background-color: white;
border-radius: 4px;
border-width: 1px;
border-style: solid;
border-color: var(--white_darker);
box-shadow:
0px 0px 0px 0px rgba(0, 0, 0, 0.2),
0px 0px 0px 0px rgba(0, 0, 0, 0.14),
0px 0px 0px 0px rgba(0,0,0,.12);
}
.box-menu-action {
list-style: none;
display: flex;
align-items: center;
}
.item-menu-action:hover {
cursor: pointer;
}
.item-menu-action > .icon {
margin: 1rem 1rem;
}
.list-box-item {
padding: 0;
padding: 1rem 0 3rem 0;
max-height: 8rem;
overflow-y: auto;
}
.list-box-item.empty {
padding: 0;
}
.scroll-container::-webkit-scrollbar {
width: 5px;
}
.scroll-container::-webkit-scrollbar-track {
background: #ddd;
}
.scroll-container::-webkit-scrollbar-thumb {
background: #666;
}
li.item {
font-size: 0.75rem;
display: grid;
grid-template-columns: 0 1fr auto auto ;
position: relative;
align-items: center;
justify-content: flex-start;
padding: 0.5rem 0.75rem 0.5rem 1.25rem;
list-style: none;
}
.head-divider {
margin: 16px;
width: auto;
height: 1px;
background-color: var(--white_darker);
}
.menu-more-container {
z-index: 1;
}
.menu-bottom {
display: flex;
justify-content: center;
align-items: center;
margin-right: 1rem;
}
.add {
background-color: var(--white_light);
padding: 0.75rem;
margin-right: 0;
margin-left: auto;
border-radius: 50%;
}
.add:hover {
cursor: pointer;
background-color: var(--white_darker);
}
.menu-btn {
display: flex;
justify-content: center;
align-items: center;
padding: 0.1rem;
margin-right: 0;
margin-left: auto;
border-radius: 50%;
}
.menu-btn.box {
font-size: 1.25rem;
}
.menu-btn.item {
font-size: 1rem;
}
.menu-btn:hover {
cursor: pointer;
}
.menu-more-list {
padding: 0;
position: absolute;
z-index: 1;
background-color: white;
border-radius: 4px;
border-width: 1px;
border-style: solid;
border-color: var(--white_darker);
box-shadow:
0px 0px 0px 0px rgba(0, 0, 0, 0.2),
0px 0px 0px 0px rgba(0, 0, 0, 0.14),
0px 0px 0px 0px rgba(0,0,0,.12);
}
.menu-more-list.box {
width: 120px;
margin: 0 10px 0 0;
top: 4rem;
right: 0;
}
.menu-more-list.item {
width: 4.8rem;
margin: 0 10px 0 0;
top: 2rem;
right: 0;
}
.menu-more-list.item > .menu-action > .icon {
margin: 0.2rem 0.2rem;
}
.menu-action {
z-index: inherit;
list-style: none;
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
}
.menu-action:hover {
cursor: pointer;
background-color: var(--white_darker);
}
.menu-action > .icon {
margin: 0.6rem 0.6rem;
}
.header-container {
display: flex;
justify-content: center;
align-items: center;
}
-17
View File
@@ -1,17 +0,0 @@
button.button {
height: 2.5rem;
display: flex;
font-size: .875rem;
text-transform: uppercase;
padding: 0.75rem;
align-items: center;
justify-content: center;
min-width: 64px;
background-color: var(--white_light);
border: 1px solid var(--black_light);
border-radius: 4px;
}
button.button:hover {
background-color: var(--white_darker);
}
-10
View File
@@ -1,10 +0,0 @@
label.input {
display: block;
margin: 0 0 0.4rem 0;
}
input.input {
border: 1px solid grey;
border-radius: 0.2rem;
padding: 0.4rem 0.6rem;
margin: 0 0 0.8rem 0;
}
-65
View File
@@ -1,65 +0,0 @@
.modal-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.32);
z-index: 1000;
padding: 20px;
overflow: auto;
}
.modal-content {
background-color: white;
border-radius: 1em;
padding: 20px;
max-width: 500px;
width: 100%;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.modal-action {
display: grid;
gap: 0.2rem;
grid-template-columns: auto auto;
justify-content: end;
padding: 0.2rem;
}
.btn {
background-color: white;
}
.btn-close:hover {
cursor: pointer;
background-color: var(--white_darker);
}
.color-selector-container {
display: grid;
grid-auto-flow: column;
width: fit-content;
gap:3px;
}
.color-selector-container > .color {
border-radius: 50%;
width: 2em;
height: 2em;
margin-right: 0.2rem;
border-style:solid;
border-color: black;
border-width: 1px;
}
.color-selector-container > .color.selected {
border-style: solid;
border-color: var(--black_light);
}
.color-selector-container > .color:hover {
cursor: pointer;
}
.color-selector-container > .color > input[type=radio] {
visibility: hidden;
}
-13
View File
@@ -1,13 +0,0 @@
label.textarea {
display: block;
margin: 0 0 0.4rem 0;
}
textarea.textarea {
resize: none;
width: 90%;
height: 6em;
border: 1px solid grey;
border-radius: 0.2rem;
padding: 0.4rem 0.6rem;
margin: 0 0 0.8rem 0;
}
+5 -1
View File
@@ -1,7 +1,11 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
plugins: [
react(),
tailwindcss(),
],
})
+100 -26
View File
@@ -56,19 +56,60 @@ app.post('/boxes', async (request, response) => {
try {
const numBoxes = await Box.countDocuments();
const newBox = await Box.create({
...request.body,
sort: numBoxes,
...request.body,
sort: numBoxes,
});
response.status(201).json(newBox);
} catch (error) {
}
catch (error) {
console.error('Error creating box:', error);
response.status(500).json({ error: 'Failed to create box' });
}
}
});
// update box
app.put('/boxes/:boxId', async (request, response) => {
try {
const { boxId } = request.params;
const updatedBox = await Box.findByIdAndUpdate(
boxId,
{ ...request.body },
{ new: true, runValidators: true }
);
if (!updatedBox) {
return response.status(404).json({ error: 'Box not found' });
}
response.status(200).json(updatedBox);
}
catch (error) {
console.error('Error updating box:', error);
response.status(500).json({ error: 'Failed to update box' });
}
});
//create item
// delete box
app.delete('/boxes/:boxId', async (request, response) => {
try {
const { boxId } = request.params;
const result = await Box.findByIdAndDelete(boxId);
if (!result) {
return response.status(404).json({ success: false, message: 'Box not found' });
}
response.status(200).json({ success: true, message: 'Box deleted', data: result });
}
catch (error) {
console.error('Error deleting box:', error);
response.status(500).json({ success: false, error: 'Failed to delete box' });
}
});
// create item
app.post('/boxes/:boxId/items', async (request, response) => {
try {
const item = new Item(request.body);
@@ -84,32 +125,65 @@ app.post('/boxes/:boxId/items', async (request, response) => {
response.send(item);
} catch (error) {
console.error('Error adding item to Box:', error);
throw error; // or handle it according to your needs
throw error;
}
});
//edit item
app.put('/box/:boxId/item/:itemId', async (request, response) => {
const boxID = request.params.boxId;
const itemID = request.params.itemId;
await Box.updateOne({ '_id': boxID, 'item._id': itemID }, {
$set: {
'item.$.name': request.body.name,
'item.$.description': request.body.description,
'item.$.quantity': request.body.quantity
// edit item
app.put('/boxes/:boxId/items/:itemId', async (request, response) => {
const { boxId, itemId } = request.params;
const { name, description, quantity } = request.body;
try {
const updatedBox = await Box.findOneAndUpdate(
{ _id: boxId, 'items._id': itemId },
{
$set: {
'items.$.name': name,
'items.$.description': description,
'items.$.quantity': quantity,
},
},
{ new: true }
);
const updatedItem = updatedBox.items.find(item => item._id.toString() === itemId);
if (!updatedItem) {
return response.status(404).json({ error: 'Box or item not found' });
}
})
const updatedBox = await getBox(boxID);
response.send(updatedBox);
response.status(200).json(updatedItem);
}
catch (error) {
console.error('Error updating item:', error);
response.status(500).json({ error: 'Failed to update item' });
}
});
//delete item
app.delete('/box/:boxId/item/:itemId', async (request, response) => {
const boxID = request.params.boxId;
const itemID = request.params.itemId;
await Box.update({ _id: boxID }, { $pull: { item: { _id: itemID } } });
const box = await getBox(boxID);
response.send(box);
// delete item from box
app.delete('/boxes/:boxId/items/:itemId', async (request, response) => {
const { boxId, itemId } = request.params;
try {
const updatedBox = await Box.findByIdAndUpdate(
boxId,
{
$pull: { items: { _id: itemId } }
},
{ new: true }
);
if (!updatedBox) {
return response.status(404).json({ error: 'Box not found' });
}
response.json(updatedBox);
}
catch (error) {
console.error('Error deleting item:', error);
response.status(500).json({ error: 'Failed to delete item' });
}
});
//move item
app.post('/box/item/:itemId/:boxIdTarget', async (request, response) => {
const itemID = request.params.itemId;