diff --git a/frontend/index.html b/frontend/index.html index 3191de0eb..d12fb2a8a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,4 +1,4 @@ - + @@ -6,12 +6,16 @@ urBackend | Instant Backend for Developers - - - + + + +
- \ No newline at end of file + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d22844062..ec613968a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,13 +8,15 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@tailwindcss/vite": "^4.1.18", "axios": "^1.13.2", "lucide-react": "^0.554.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-hot-toast": "^2.6.0", "react-router-dom": "^7.9.6", - "recharts": "^3.5.1" + "recharts": "^3.5.1", + "tailwindcss": "^4.1.18" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -317,7 +319,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -334,7 +335,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -351,7 +351,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -368,7 +367,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -385,7 +383,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -402,7 +399,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -419,7 +415,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -436,7 +431,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -453,7 +447,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -470,7 +463,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -487,7 +479,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -504,7 +495,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -521,7 +511,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -538,7 +527,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -555,7 +543,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -572,7 +559,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -589,7 +575,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -606,7 +591,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -623,7 +607,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -640,7 +623,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -657,7 +639,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -674,7 +655,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -691,7 +671,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -708,7 +687,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -725,7 +703,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -742,7 +719,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -965,7 +941,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -976,7 +951,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -987,7 +961,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -997,14 +970,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1061,7 +1032,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1075,7 +1045,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1089,7 +1058,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1103,7 +1071,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1117,7 +1084,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1131,7 +1097,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1145,7 +1110,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1159,7 +1123,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1173,7 +1136,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1187,7 +1149,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1201,7 +1162,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1215,7 +1175,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1229,7 +1188,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1243,7 +1201,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1257,7 +1214,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1271,7 +1227,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1285,7 +1240,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1299,7 +1253,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1313,7 +1266,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1327,7 +1279,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1341,7 +1292,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1355,7 +1305,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1374,6 +1323,263 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1486,7 +1692,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -1992,6 +2197,15 @@ "node": ">=0.4.0" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "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", @@ -2013,6 +2227,19 @@ "dev": true, "license": "ISC" }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "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", @@ -2072,7 +2299,6 @@ "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -2348,7 +2574,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -2453,7 +2678,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, @@ -2567,6 +2791,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", @@ -2719,6 +2949,15 @@ "dev": true, "license": "ISC" }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "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", @@ -2810,6 +3049,255 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "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-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "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", @@ -2852,6 +3340,15 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2906,7 +3403,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", @@ -3022,14 +3518,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/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -3042,7 +3536,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -3274,7 +3767,6 @@ "version": "4.53.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -3361,7 +3853,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" @@ -3393,6 +3884,25 @@ "node": ">=8" } }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -3403,7 +3913,6 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -3505,7 +4014,6 @@ "version": "7.2.4", "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz", "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==", - "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", diff --git a/frontend/package.json b/frontend/package.json index fc7dbb1d3..cb00bb3a1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,13 +10,15 @@ "preview": "vite preview" }, "dependencies": { + "@tailwindcss/vite": "^4.1.18", "axios": "^1.13.2", "lucide-react": "^0.554.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-hot-toast": "^2.6.0", "react-router-dom": "^7.9.6", - "recharts": "^3.5.1" + "recharts": "^3.5.1", + "tailwindcss": "^4.1.18" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/frontend/src/index.css b/frontend/src/index.css index 84fae0498..889160cfa 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,4 +1,5 @@ /* --- GLOBAL DESIGN SYSTEM (Premium / Supabase-like) --- */ +@import "tailwindcss"; :root { /* Colors - Core Black Theme (Supabase-like) */ --color-primary: #3ECF8E; @@ -518,4 +519,12 @@ body { flex: 1; padding: 2rem; overflow-y: auto; +} +.confirmationModal{ + padding: 1rem; +} +.confirmationModalBtn{ + padding: 5px; + width: 5rem; + } \ No newline at end of file diff --git a/frontend/src/pages/ConfirmationModal.jsx b/frontend/src/pages/ConfirmationModal.jsx new file mode 100644 index 000000000..4aac05ca3 --- /dev/null +++ b/frontend/src/pages/ConfirmationModal.jsx @@ -0,0 +1,50 @@ +import React from "react"; +import { AlertTriangle } from "lucide-react"; + +function ConfirmationModal({ open, title, message, onConfirm, onCancel }) { + if (!open) return null; + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+ + +
+ + {/* Message */} + + + {/* Buttons */} +
+ + + +
+
+
+ ); +} + +export default ConfirmationModal; \ No newline at end of file diff --git a/frontend/src/pages/Database.jsx b/frontend/src/pages/Database.jsx index 7c10a6c97..e1a9d26f1 100644 --- a/frontend/src/pages/Database.jsx +++ b/frontend/src/pages/Database.jsx @@ -1,389 +1,500 @@ -import { useState, useEffect, useCallback } from 'react'; -import { useParams, useSearchParams, useNavigate } from 'react-router-dom'; -import axios from 'axios'; -import { useAuth } from '../context/AuthContext'; -import toast from 'react-hot-toast'; +import { useState, useEffect, useCallback } from "react"; +import { useParams, useSearchParams, useNavigate } from "react-router-dom"; +import axios from "axios"; +import { useAuth } from "../context/AuthContext"; +import toast from "react-hot-toast"; +import ConfirmationModal from "./ConfirmationModal"; import { - Database as DbIcon, Plus, Trash2, RefreshCw, - Code, Table as TableIcon, Search, Menu, X, - ChevronRight, MoreVertical, FileText -} from 'lucide-react'; -import { API_URL } from '../config'; + Database as DbIcon, + Plus, + Trash2, + RefreshCw, + Code, + Table as TableIcon, + Search, + Menu, + X, + ChevronRight, + MoreVertical, + FileText, +} from "lucide-react"; +import { API_URL } from "../config"; export default function Database() { - const { projectId } = useParams(); - const [searchParams, setSearchParams] = useSearchParams(); - const navigate = useNavigate(); - const { token } = useAuth(); - - const [project, setProject] = useState(null); - const [collections, setCollections] = useState([]); - const [activeCollection, setActiveCollection] = useState(null); - const [data, setData] = useState([]); - const [loadingData, setLoadingData] = useState(false); - const [viewMode, setViewMode] = useState('table'); - const [isAddModalOpen, setIsAddModalOpen] = useState(false); - const [newData, setNewData] = useState({}); - const [isSidebarOpen, setIsSidebarOpen] = useState(false); - - // Fetch Project & Collections - useEffect(() => { - const fetchProject = async () => { - try { - const res = await axios.get(`${API_URL}/api/projects/${projectId}`, { - headers: { Authorization: `Bearer ${token}` } - }); - setProject(res.data); - setCollections(res.data.collections || []); - - const queryCollection = searchParams.get('collection'); - if (queryCollection) { - const found = res.data.collections.find(c => c.name === queryCollection); - if (found) setActiveCollection(found); - } else if (res.data.collections.length > 0) { - setActiveCollection(res.data.collections[0]); - } - } catch { - toast.error("Failed to load project"); - } - }; - fetchProject(); - }, [projectId, token, searchParams]); - - const fetchData = useCallback(async () => { - if (!activeCollection) return; - setLoadingData(true); - try { - const res = await axios.get(`${API_URL}/api/projects/${projectId}/collections/${activeCollection.name}/data`, { - headers: { Authorization: `Bearer ${token}` } - }); - setData(res.data); - } catch (err) { - console.error(err); - toast.error("Failed to load data"); - } finally { - setLoadingData(false); - } - }, [activeCollection, projectId, token]); - - // Fetch Data on Collection Change - useEffect(() => { - if (!activeCollection) return; - setSearchParams({ collection: activeCollection.name }); - fetchData(); - if (window.innerWidth <= 768) setIsSidebarOpen(false); - }, [activeCollection, fetchData, setSearchParams]); - - const handleDelete = async (id) => { - if (!window.confirm("Are you sure you want to delete this document?")) return; - try { - await axios.delete(`${API_URL}/api/projects/${projectId}/collections/${activeCollection.name}/data/${id}`, { - headers: { Authorization: `Bearer ${token}` } - }); - setData(prev => prev.filter(item => item._id !== id)); - toast.success("Document deleted"); - } catch (err) { - console.error(err); - toast.error("Failed to delete document"); + const { projectId } = useParams(); + const [searchParams, setSearchParams] = useSearchParams(); + const navigate = useNavigate(); + const { token } = useAuth(); + + const [project, setProject] = useState(null); + const [collections, setCollections] = useState([]); + const [activeCollection, setActiveCollection] = useState(null); + const [data, setData] = useState([]); + const [loadingData, setLoadingData] = useState(false); + const [viewMode, setViewMode] = useState("table"); + const [isAddModalOpen, setIsAddModalOpen] = useState(false); + const [newData, setNewData] = useState({}); + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + //used showModal to open the Confirmation model + const [showModal, setShowModal] = useState(false); + //keeping track of the selected record in the collection + const [selectedId, setSelectedId] = useState(null); + const fetchShowModal = (id) => { + setShowModal(true); + setSelectedId(id); + }; + // Fetch Project & Collections + useEffect(() => { + const fetchProject = async () => { + try { + const res = await axios.get(`${API_URL}/api/projects/${projectId}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + setProject(res.data); + setCollections(res.data.collections || []); + + const queryCollection = searchParams.get("collection"); + if (queryCollection) { + const found = res.data.collections.find( + (c) => c.name === queryCollection + ); + if (found) setActiveCollection(found); + } else if (res.data.collections.length > 0) { + setActiveCollection(res.data.collections[0]); } + } catch { + toast.error("Failed to load project"); + } }; - - const handleAddDocument = async (e) => { - e.preventDefault(); - try { - const formattedData = { ...newData }; - (activeCollection.model || []).forEach(field => { - if (field.type === 'Number') formattedData[field.key] = Number(formattedData[field.key]); - if (field.type === 'Boolean') formattedData[field.key] = formattedData[field.key] === 'true'; - }); - - await axios.post(`${API_URL}/api/projects/${projectId}/collections/${activeCollection.name}/data`, formattedData, { - headers: { Authorization: `Bearer ${token}` } - }); - - toast.success("Document added successfully"); - setIsAddModalOpen(false); - setNewData({}); - fetchData(); - } catch (err) { - console.error(err); - toast.error(err.response?.data?.error || "Failed to add data"); + fetchProject(); + }, [projectId, token, searchParams]); + + const fetchData = useCallback(async () => { + if (!activeCollection) return; + setLoadingData(true); + try { + const res = await axios.get( + `${API_URL}/api/projects/${projectId}/collections/${activeCollection.name}/data`, + { + headers: { Authorization: `Bearer ${token}` }, } - }; - - const renderInput = (field) => { - const val = newData[field.key] || ''; - if (field.type === 'Boolean') { - return ( -
- -
- ); + ); + setData(res.data); + } catch (err) { + console.error(err); + toast.error("Failed to load data"); + } finally { + setLoadingData(false); + } + }, [activeCollection, projectId, token]); + + // Fetch Data on Collection Change + useEffect(() => { + if (!activeCollection) return; + setSearchParams({ collection: activeCollection.name }); + fetchData(); + if (window.innerWidth <= 768) setIsSidebarOpen(false); + }, [activeCollection, fetchData, setSearchParams]); + + const handleDelete = async (id) => { + // if (!window.confirm("Are you sure you want to delete this document?")) + // return; + try { + await axios.delete( + `${API_URL}/api/projects/${projectId}/collections/${activeCollection.name}/data/${id}`, + { + headers: { Authorization: `Bearer ${token}` }, } - return ( - setNewData({ ...newData, [field.key]: e.target.value })} - required={field.required} - /> - ); - }; - - // --- SUB-COMPONENTS --- // - - const Sidebar = () => ( - - ); - - const TableView = () => ( -
- - - - - {(activeCollection.model || []).map(field => ( - - ))} - - - - - - {data.map((row, i) => ( - - - {(activeCollection.model || []).map(field => ( - - ))} - - - - ))} - -
# -
- {field.key} - {field.type} -
-
IDActions
{i + 1} - {typeof row[field.key] === 'boolean' ? ( - - {String(row[field.key])} - - ) : ( - {String(row[field.key])} - )} - - {row._id.substring(0, 8)}... - - -
+ ); + setData((prev) => prev.filter((item) => item._id !== id)); + toast.success("Document deleted"); + } catch (err) { + console.error(err); + toast.error("Failed to delete document"); + } + }; + + const handleAddDocument = async (e) => { + e.preventDefault(); + try { + const formattedData = { ...newData }; + (activeCollection.model || []).forEach((field) => { + if (field.type === "Number") + formattedData[field.key] = Number(formattedData[field.key]); + if (field.type === "Boolean") + formattedData[field.key] = formattedData[field.key] === "true"; + }); + + await axios.post( + `${API_URL}/api/projects/${projectId}/collections/${activeCollection.name}/data`, + formattedData, + { + headers: { Authorization: `Bearer ${token}` }, + } + ); + + toast.success("Document added successfully"); + setIsAddModalOpen(false); + setNewData({}); + fetchData(); + } catch (err) { + console.error(err); + toast.error(err.response?.data?.error || "Failed to add data"); + } + }; + + const renderInput = (field) => { + const val = newData[field.key] || ""; + if (field.type === "Boolean") { + return ( +
+
+ ); + } + return ( + + setNewData({ ...newData, [field.key]: e.target.value }) + } + required={field.required} + /> ); - - const JsonView = () => ( -
-
-                {JSON.stringify(data, null, 2)}
-            
+ }; + + // --- SUB-COMPONENTS --- // + + const Sidebar = () => ( + + ); + + const TableView = () => ( +
+ + + + + {(activeCollection.model || []).map((field) => ( + ))} + + + + + + {data.map((row, i) => ( + + + {(activeCollection.model || []).map((field) => ( + + ))} + + + + ))} + + {showModal && ( + { + handleDelete(selectedId); + setShowModal(false); + }} + onCancel={() => setShowModal(false)} + /> + )} +
# +
+ {field.key} + {field.type}
+
IDActions
{i + 1} + {typeof row[field.key] === "boolean" ? ( + + {String(row[field.key])} + + ) : ( + {String(row[field.key])} + )} + + {row._id.substring(0, 8)}... + + +
+
+ ); + + const JsonView = () => ( +
+
{JSON.stringify(data, null, 2)}
+
+ ); + + const SkeletonLoader = () => ( +
+ {[1, 2, 3, 4, 5].map((i) => ( +
+
- ); + ))} +
+ ); + + return ( +
+ {/* Mobile Overlay */} +
setIsSidebarOpen(false)} + /> + + + +
+ {activeCollection ? ( + <> +
+
+ +
+
+ {project?.name} + / + {activeCollection.name} +
+

{activeCollection.name}

+
+
+ +
+ {data.length} Records + +
+ + +
- return ( -
- {/* Mobile Overlay */} -
setIsSidebarOpen(false)} - /> - - - -
- {activeCollection ? ( - <> -
-
- -
-
- {project?.name} - / - {activeCollection.name} -
-

{activeCollection.name}

-
-
- -
- {data.length} Records - -
- - -
- - - - -
-
- -
- {loadingData ? ( - - ) : data.length === 0 ? ( -
-
- -
-

No data found

-

Start by adding your first document to this collection.

- -
- ) : ( - viewMode === 'table' ? : - )} -
- - ) : ( -
- -
- -

Select a Collection

-

Choose a collection from the sidebar to manage your data.

-
-
- )} -
- - {/* Add Document Modal */} - {isAddModalOpen && ( -
-
-
-

Add New Document

- -
-
- {(activeCollection.model || []).map(field => ( -
- - {renderInput(field)} -
- ))} -
- - -
-
-
+ + + +
+
+ +
+ {loadingData ? ( + + ) : data.length === 0 ? ( +
+
+ +
+

No data found

+

Start by adding your first document to this collection.

+ +
+ ) : viewMode === "table" ? ( + + ) : ( + + )} +
+ + ) : ( +
+ +
+ +

Select a Collection

+

+ Choose a collection from the sidebar to manage your data. +

+
+
+ )} +
+ + {/* Add Document Modal */} + {isAddModalOpen && ( +
+
+
+

Add New Document

+ +
+
+ {(activeCollection.model || []).map((field) => ( +
+ + {renderInput(field)}
- )} + ))} +
+ + +
+
+
+
+ )} - -
- ); +
+ ); } \ No newline at end of file diff --git a/frontend/src/pages/ProjectSettings.jsx b/frontend/src/pages/ProjectSettings.jsx index a51f9ad9b..908c7f2f7 100644 --- a/frontend/src/pages/ProjectSettings.jsx +++ b/frontend/src/pages/ProjectSettings.jsx @@ -1,355 +1,710 @@ -import { useState, useEffect } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; -import axios from 'axios'; -import { useAuth } from '../context/AuthContext'; -import toast from 'react-hot-toast'; -import { Trash2, AlertTriangle, Save, CheckCircle } from 'lucide-react'; -import { API_URL } from '../config'; +import { useState, useEffect } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import axios from "axios"; +import { useAuth } from "../context/AuthContext"; +import toast from "react-hot-toast"; +import { Trash2, AlertTriangle, Save, CheckCircle } from "lucide-react"; +import { API_URL } from "../config"; +import ConfirmationModal from "./ConfirmationModal"; export default function ProjectSettings() { - const { projectId } = useParams(); - const { token } = useAuth(); - const navigate = useNavigate(); - - // Existing State - const [project, setProject] = useState(null); - const [loading, setLoading] = useState(true); - const [deleteConfirm, setDeleteConfirm] = useState(''); - - // --- NEW STATE FOR RENAME --- - const [newName, setNewName] = useState(''); - const [renaming, setRenaming] = useState(false); - - useEffect(() => { - const fetchProject = async () => { - try { - const res = await axios.get(`${API_URL}/api/projects/${projectId}`, { - headers: { Authorization: `Bearer ${token}` } - }); - setProject(res.data); - // Set initial name for renaming - setNewName(res.data.name); - } catch { - toast.error("Failed to load project"); - } finally { - setLoading(false); - } - }; - fetchProject(); - }, [projectId, token]); - - // --- NEW: HANDLE RENAME --- - const handleRename = async () => { - if (!newName.trim()) return toast.error("Project name cannot be empty"); - - setRenaming(true); - try { - await axios.patch(`${API_URL}/api/projects/${projectId}`, - { name: newName }, - { headers: { Authorization: `Bearer ${token}` } } - ); - toast.success("Project renamed successfully!"); - // Update local state to reflect change immediately - setProject(prev => ({ ...prev, name: newName })); - } catch { - toast.error("Failed to rename project"); - } finally { - setRenaming(false); - } - } + const { projectId } = useParams(); + const { token } = useAuth(); + const navigate = useNavigate(); + // Used to control the Confirmation modal visibility + const [showModal, setShowModal] = useState(false); - const handleDeleteProject = async () => { - if (deleteConfirm !== project.name) return toast.error("Project name does not match"); + // Existing State + const [project, setProject] = useState(null); + const [loading, setLoading] = useState(true); + const [deleteConfirm, setDeleteConfirm] = useState(""); - if (!confirm("Final warning: This will delete the project and all its data.")) return; + // --- NEW STATE FOR RENAME --- + const [newName, setNewName] = useState(""); + const [renaming, setRenaming] = useState(false); - try { - await axios.delete(`${API_URL}/api/projects/${projectId}`, { - headers: { Authorization: `Bearer ${token}` } - }); - toast.success("Project deleted"); - navigate('/dashboard'); - } catch { - toast.error("Failed to delete project"); - } + useEffect(() => { + const fetchProject = async () => { + try { + const res = await axios.get(`${API_URL}/api/projects/${projectId}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + setProject(res.data); + // Set initial name for renaming + setNewName(res.data.name); + } catch { + toast.error("Failed to load project"); + } finally { + setLoading(false); + } }; + fetchProject(); + }, [projectId, token]); - if (loading) return
Loading...
; - - return ( -
-
-
-

- Project Settings -

-

Configuration and preferences for {project?.name}.

-
-
+ // --- NEW: HANDLE RENAME --- + const handleRename = async () => { + if (!newName.trim()) return toast.error("Project name cannot be empty"); - {/* General Settings (Rename Feature) */} -
-

-
- General Information -

-
-
- - setNewName(e.target.value)} - style={{ width: '100%', padding: '12px', background: 'var(--color-bg-input)', border: '1px solid var(--color-border)', borderRadius: '8px', color: '#fff' }} - /> -
- -
-
+ setRenaming(true); + try { + await axios.patch( + `${API_URL}/api/projects/${projectId}`, + { name: newName }, + { headers: { Authorization: `Bearer ${token}` } } + ); + toast.success("Project renamed successfully!"); + // Update local state to reflect change immediately + setProject((prev) => ({ ...prev, name: newName })); + } catch { + toast.error("Failed to rename project"); + } finally { + setRenaming(false); + } + }; - {/* External Configuration */} -
- - -
+ const handleDeleteProject = async () => { + if (deleteConfirm !== project.name) + return toast.error("Project name does not match"); - {/* Danger Zone */} -
-
- -

Danger Zone

-
- -

- This action cannot be undone. This will permanently delete the - {project?.name} project and all associated data including collections, files, and users. -

- -
-
- - setDeleteConfirm(e.target.value)} - style={{ width: '100%', padding: '12px', background: 'var(--color-bg-input)', border: '1px solid rgba(234, 84, 85, 0.3)', borderRadius: '8px', color: '#fff' }} - /> -
- -
-
+ // // if ( + // // !confirm("Final warning: This will delete the project and all its data.") + // // ) + // return; + + try { + await axios.delete(`${API_URL}/api/projects/${projectId}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + toast.success("Project deleted"); + navigate("/dashboard"); + } catch { + toast.error("Failed to delete project"); + } + }; + + if (loading) return
Loading...
; + + return ( +
+
+
+

+ Project Settings +

+

+ Configuration and preferences for {project?.name}. +

- ); +
+ + {/* General Settings (Rename Feature) */} +
+

+
+ General Information +

+
+
+ + setNewName(e.target.value)} + style={{ + width: "100%", + padding: "12px", + background: "var(--color-bg-input)", + border: "1px solid var(--color-border)", + borderRadius: "8px", + color: "#fff", + }} + /> +
+ +
+
+ + {/* External Configuration */} +
+ + +
+ + {/* Danger Zone */} +
+
+ +

Danger Zone

+
+ +

+ This action cannot be undone. This will permanently delete the + {project?.name} project + and all associated data including collections, files, and users. +

+ +
+
+ + setDeleteConfirm(e.target.value)} + style={{ + width: "100%", + padding: "12px", + background: "var(--color-bg-input)", + border: "1px solid rgba(234, 84, 85, 0.3)", + borderRadius: "8px", + color: "#fff", + }} + /> +
+ + + {showModal && ( + { + handleDeleteProject(); + setShowModal(false); + }} + onCancel={() => setShowModal(false)} + /> + )} +
+
+
+ ); } function DatabaseConfigForm({ project, projectId, token }) { - const [dbUri, setDbUri] = useState(''); - const [loading, setLoading] = useState(false); - // Use optional chaining carefully - project might be null initially - const isConfigured = project?.resources?.db?.isExternal || false; - const [showForm, setShowForm] = useState(!isConfigured); - - useEffect(() => { - const configured = project?.resources?.db?.isExternal || false; - setShowForm(!configured); - }, [project]); - - const handleUpdate = async () => { - if (!dbUri) return toast.error("Database URI is required"); - - setLoading(true); - try { - await axios.patch(`${API_URL}/api/projects/${projectId}/byod-config`, - { dbUri }, - { headers: { Authorization: `Bearer ${token}` } } - ); - toast.success("Database configuration updated!"); - setShowForm(false); - setDbUri(''); - // Typically we'd reload project data here, but for now we rely on user refresh or optimistic ui if needed - // Ideally notify parent to refresh project, but basic flow: - } catch (err) { - toast.error(err.response?.data?.error || "Failed to update DB config"); - } finally { - setLoading(false); - } - }; + const [dbUri, setDbUri] = useState(""); + const [loading, setLoading] = useState(false); + // Use optional chaining carefully - project might be null initially + const isConfigured = project?.resources?.db?.isExternal || false; + const [showForm, setShowForm] = useState(!isConfigured); - return ( -
-
- -
-

- Database Configuration (MongoDB) -

-

- Connect your own MongoDB cluster. -

-
+ useEffect(() => { + const configured = project?.resources?.db?.isExternal || false; + setShowForm(!configured); + }, [project]); + + const handleUpdate = async () => { + if (!dbUri) return toast.error("Database URI is required"); + + setLoading(true); + try { + await axios.patch( + `${API_URL}/api/projects/${projectId}/byod-config`, + { dbUri }, + { headers: { Authorization: `Bearer ${token}` } } + ); + toast.success("Database configuration updated!"); + setShowForm(false); + setDbUri(""); + // Typically we'd reload project data here, but for now we rely on user refresh or optimistic ui if needed + // Ideally notify parent to refresh project, but basic flow: + } catch (err) { + toast.error(err.response?.data?.error || "Failed to update DB config"); + } finally { + setLoading(false); + } + }; + + return ( +
+
- {isConfigured && !showForm ? ( -
-
- - Connected -
- -
- ) : ( -
-
- - setDbUri(e.target.value)} - style={{ width: '100%', padding: '12px', background: 'var(--color-bg-input)', border: '1px solid var(--color-border)', borderRadius: '8px', color: '#fff', fontFamily: 'monospace' }} - /> -
-
- {isConfigured && } - -
-
+
+

+ Database Configuration + (MongoDB) +

+

+ Connect your own MongoDB cluster. +

+
+ + {isConfigured && !showForm ? ( +
+
+ + Connected +
+ +
+ ) : ( +
+
+ + setDbUri(e.target.value)} + style={{ + width: "100%", + padding: "12px", + background: "var(--color-bg-input)", + border: "1px solid var(--color-border)", + borderRadius: "8px", + color: "#fff", + fontFamily: "monospace", + }} + /> +
+
+ {isConfigured && ( + )} + +
- ); + )} +
+ ); } function StorageConfigForm({ project, projectId, token }) { - const [config, setConfig] = useState({ - storageUrl: '', - storageKey: '', - storageProvider: 'supabase' - }); - const [loading, setLoading] = useState(false); - const isConfigured = project?.resources?.storage?.isExternal || false; - const [showForm, setShowForm] = useState(!isConfigured); - - useEffect(() => { - const configured = project?.resources?.storage?.isExternal || false; - setShowForm(!configured); - }, [project]); - - const handleChange = (e) => setConfig({ ...config, [e.target.name]: e.target.value }); - - const handleUpdate = async () => { - if (!config.storageUrl || !config.storageKey) return toast.error("URL and Key are required"); - - setLoading(true); - try { - await axios.patch(`${API_URL}/api/projects/${projectId}/byod-config`, - config, - { headers: { Authorization: `Bearer ${token}` } } - ); - toast.success("Storage configuration updated!"); - setShowForm(false); - setConfig({ storageUrl: '', storageKey: '', storageProvider: 'supabase' }); - } catch (err) { - toast.error(err.response?.data?.error || "Failed to update Storage config"); - } finally { - setLoading(false); - } - }; + const [config, setConfig] = useState({ + storageUrl: "", + storageKey: "", + storageProvider: "supabase", + }); + const [loading, setLoading] = useState(false); + const isConfigured = project?.resources?.storage?.isExternal || false; + const [showForm, setShowForm] = useState(!isConfigured); - return ( -
-
- -
-

- Storage Configuration -

-

- Connect your own S3-compatible storage. -

-
+ useEffect(() => { + const configured = project?.resources?.storage?.isExternal || false; + setShowForm(!configured); + }, [project]); + + const handleChange = (e) => + setConfig({ ...config, [e.target.name]: e.target.value }); - {isConfigured && !showForm ? ( -
-
- - Connected -
- -
- ) : ( -
-
-
- - -
-
- - -
-
-
- - -
-
- {isConfigured && } - -
-
+ const handleUpdate = async () => { + if (!config.storageUrl || !config.storageKey) + return toast.error("URL and Key are required"); + + setLoading(true); + try { + await axios.patch( + `${API_URL}/api/projects/${projectId}/byod-config`, + config, + { headers: { Authorization: `Bearer ${token}` } } + ); + toast.success("Storage configuration updated!"); + setShowForm(false); + setConfig({ + storageUrl: "", + storageKey: "", + storageProvider: "supabase", + }); + } catch (err) { + toast.error( + err.response?.data?.error || "Failed to update Storage config" + ); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ +
+

+ Storage Configuration +

+

+ Connect your own S3-compatible storage. +

+
+ + {isConfigured && !showForm ? ( +
+
+ + Connected +
+ +
+ ) : ( +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ {isConfigured && ( + )} + +
- ); -} \ No newline at end of file + )} +
+ ); +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 8b0f57b91..77d2ab8bc 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,7 +1,10 @@ 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(),], })