diff --git a/Frontend/.env b/Frontend/.env new file mode 100644 index 0000000..77b2686 --- /dev/null +++ b/Frontend/.env @@ -0,0 +1 @@ +VITE_API_URL=http://192.168.29.61:8081 diff --git a/Frontend/index.html b/Frontend/index.html index b6120e1..f4f4d16 100644 --- a/Frontend/index.html +++ b/Frontend/index.html @@ -8,7 +8,7 @@ - Drive-thru + Skycrate diff --git a/Frontend/package-lock.json b/Frontend/package-lock.json index fa6a50a..91022ce 100644 --- a/Frontend/package-lock.json +++ b/Frontend/package-lock.json @@ -1,18 +1,20 @@ { - "name": "drive-thru", + "name": "Skycrate", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "drive-thru", + "name": "Skycrate", "version": "0.0.0", "dependencies": { "@reduxjs/toolkit": "^2.6.0", "@tailwindcss/vite": "^4.0.9", + "axios": "^1.8.4", "lucide-react": "^0.476.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hot-toast": "^2.5.2", "react-icons": "^5.5.0", "react-redux": "^9.2.0", "react-router-dom": "^7.2.0" @@ -1682,6 +1684,12 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -1734,6 +1742,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1804,7 +1823,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -1892,6 +1910,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1929,8 +1959,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/data-view-buffer": { "version": "1.0.2", @@ -2040,6 +2069,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -2067,7 +2105,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -2164,7 +2201,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -2173,7 +2209,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -2209,7 +2244,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -2221,7 +2255,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -2585,6 +2618,26 @@ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -2600,6 +2653,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -2630,7 +2698,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2677,7 +2744,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -2701,7 +2767,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -2767,11 +2832,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -2836,7 +2909,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -2848,7 +2920,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -2863,7 +2934,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -3680,11 +3750,31 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "engines": { "node": ">= 0.4" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4008,6 +4098,12 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4036,6 +4132,23 @@ "react": "^19.0.0" } }, + "node_modules/react-hot-toast": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz", + "integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-icons": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", diff --git a/Frontend/package.json b/Frontend/package.json index ee9f2ab..04f0317 100644 --- a/Frontend/package.json +++ b/Frontend/package.json @@ -1,5 +1,5 @@ { - "name": "drive-thru", + "name": "Skycrate", "private": true, "version": "0.0.0", "type": "module", @@ -12,9 +12,11 @@ "dependencies": { "@reduxjs/toolkit": "^2.6.0", "@tailwindcss/vite": "^4.0.9", + "axios": "^1.8.4", "lucide-react": "^0.476.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hot-toast": "^2.5.2", "react-icons": "^5.5.0", "react-redux": "^9.2.0", "react-router-dom": "^7.2.0" diff --git a/Frontend/src/components/FileList.jsx b/Frontend/src/components/FileList.jsx index 22e4d1a..03ed7b8 100644 --- a/Frontend/src/components/FileList.jsx +++ b/Frontend/src/components/FileList.jsx @@ -1,82 +1,198 @@ -import React, { useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import PropTypes from "prop-types"; +import { useDispatch, useSelector } from "react-redux"; +import { setCurrentPath } from "../store/pathSlice"; +import { + FileText, + FileVideo, + FileImage, + FileAudio, + FileArchive, + FileSpreadsheet, + FileType2, + FileCode2, + Presentation, + Folder, + Download, + Trash2, + ArrowLeft, +} from "lucide-react"; + +const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8080"; const FileTable = ({ initialPath }) => { - const [currentPath, setCurrentPath] = useState(initialPath || "/"); - const [files, setFiles] = useState([]); + // Read username dynamically to avoid stale null on first load + const username = localStorage.getItem("username") || ""; + const userRoot = `/${username}`; + + // Initialize currentPath only once with the correct userRoot + const [currentPath, setCurrentPathState] = useState( + () => initialPath || userRoot + ); + const [files, setFiles] = useState([]); + const dispatch = useDispatch(); + const isUploading = useSelector((state) => state.upload.isUploading); - // Helpers to parse entry const getType = (entry) => entry.trim().startsWith("πŸ“") ? "Folder" : "File"; const getName = (entry) => entry.trim().replace(/^πŸ“\s*|^πŸ“„\s*/, ""); const isFile = (entry) => getType(entry) === "File"; - // Fetch and show only top-level entries (indentation = 0) + const getIcon = (name, type) => { + if (type === "Folder") + return ; + + const ext = name.split(".").pop().toLowerCase(); + switch (ext) { + case "txt": + return ; + case "mp4": + case "mkv": + return ; + case "jpg": + case "jpeg": + case "png": + case "gif": + return ; + case "mp3": + case "wav": + return ; + case "zip": + case "rar": + case "tar": + case "gz": + return ; + case "csv": + case "xls": + case "xlsx": + return ; + case "ppt": + case "pptx": + return ; + case "js": + case "html": + case "css": + case "java": + case "py": + case "cpp": + return ; + default: + return ; + } + }; + const fetchFiles = async () => { try { - const response = await fetch( - `http://192.168.29.61:8080/api/hdfs/listFiles?hdfsPath=${currentPath}` + const res = await fetch( + `${API_URL}/api/hdfs/listFiles?hdfsPath=${encodeURIComponent( + currentPath + )}` ); - const data = await response.json(); - - // Filter entries: only those without leading spaces + const data = await res.json(); const filtered = data.filter( (entry) => entry.match(/^ */)[0].length === 0 ); setFiles(filtered); - } catch (error) { - console.error("Failed to fetch files:", error); + } catch (err) { + console.error("Failed to fetch files:", err); setFiles([]); } }; - useEffect(() => { - fetchFiles(); - }, [currentPath]); + const deleteFileOrFolder = async (name, type, e) => { + e.stopPropagation(); + try { + const hdfsPath = `${currentPath}/${name}`; + const endpoint = + type === "File" + ? `${API_URL}/api/hdfs/deleteFile?hdfsPath=${encodeURIComponent( + hdfsPath + )}` + : `${API_URL}/api/hdfs/deleteFolder?hdfsPath=${encodeURIComponent( + hdfsPath + )}`; - // Navigate into a folder - const handleOpenFolder = (folderName) => { - const newPath = - currentPath === "/" ? `/${folderName}` : `${currentPath}/${folderName}`; - setCurrentPath(newPath); + const resp = await fetch(endpoint, { method: "DELETE" }); + if (!resp.ok) console.error("Deletion failed:", await resp.text()); + fetchFiles(); + } catch (err) { + console.error("Failed to delete:", err); + } + }; + + useEffect(() => { + dispatch(setCurrentPath(currentPath)); + fetchFiles(); + }, [currentPath, dispatch, isUploading]); + + const handleOpenFolder = (folderName) => { + setCurrentPathState((prev) => `${prev}/${folderName}`); }; - // Go up one level const goBack = () => { - if (currentPath === "/") return; + if (currentPath === userRoot) return; const parts = currentPath.split("/").filter(Boolean); parts.pop(); - setCurrentPath(parts.length === 0 ? "/" : `/${parts.join("/")}`); + setCurrentPathState(parts.length === 0 ? userRoot : `/${parts.join("/")}`); + }; + + const handleFileDownload = async (hdfsPath, name, event) => { + event.stopPropagation(); + try { + const authToken = localStorage.getItem("token"); + const response = await fetch( + `${API_URL}/api/hdfs/downloadFile?hdfsEncPath=${encodeURIComponent( + hdfsPath + )}&localPath=${name}&username=${username}`, + { + method: "POST", + headers: { Authorization: `Bearer ${authToken}` }, + } + ); + + if (!response.ok) throw new Error(await response.text()); + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = name; + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); + fetchFiles(); + } catch (error) { + console.error("Download failed:", error); + alert("Something went wrong while downloading the file."); + } }; return ( -
-
- Path: {currentPath} - {currentPath !== "/" && ( - )}
+ - + - - - + + {files.length === 0 ? ( - @@ -84,38 +200,39 @@ const FileTable = ({ initialPath }) => { files.map((entry, idx) => { const name = getName(entry); const type = getType(entry); - const encodedPath = encodeURIComponent(`${currentPath}/${name}`); - const downloadUrl = `http://192.168.29.61:8080/api/hdfs/downloadFile?hdfsPath=${encodedPath}&localPath=E:/testdownload/${name}&kalas=${ - currentPath.split("/")[1] || "user" - }`; + const hdfsPath = `${currentPath}/${name}`; return ( handleOpenFolder(name) : undefined + } + className={`even:bg-blue-50 odd:bg-white border-b border-blue-100 transition hover:bg-blue-100 ${ + type === "Folder" ? "cursor-pointer" : "" + }`} > - - - ); diff --git a/Frontend/src/components/FileUpload.jsx b/Frontend/src/components/FileUpload.jsx deleted file mode 100644 index 2d017c8..0000000 --- a/Frontend/src/components/FileUpload.jsx +++ /dev/null @@ -1,68 +0,0 @@ -import React, { useState } from "react"; -import { uploadFileToHDFS } from "../utils/api"; - -const FileUpload = () => { - const [file, setFile] = useState(null); - const [hdfsPath, setHdfsPath] = useState("/kalas"); - const [uploadedFileName, setUploadedFileName] = useState(""); - const [username, setUsername] = useState("kalas"); - - const handleSubmit = (e) => { - e.preventDefault(); - if (!file || !uploadedFileName) { - return; - } - uploadFileToHDFS(file, hdfsPath, uploadedFileName, username); - }; - - return ( - -

Upload File Your File

- - { - setFile(e.target.files[0]); - setUploadedFileName(e.target.files[0]?.name || ""); - }} - className="mb-2" - /> - - {/* setHdfsPath(e.target.value)} - className="border p-2 mb-2 w-full" - /> */} - - {/* setUploadedFileName(e.target.value)} - className="border p-2 mb-2 w-full" - /> */} - - {/* setUsername(e.target.value)} - className="border p-2 mb-2 w-full" - /> */} - - - - ); -}; - -export default FileUpload; diff --git a/Frontend/src/components/FileUploadModal.jsx b/Frontend/src/components/FileUploadModal.jsx new file mode 100644 index 0000000..54ac590 --- /dev/null +++ b/Frontend/src/components/FileUploadModal.jsx @@ -0,0 +1,190 @@ +import { useState, useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import PropTypes from "prop-types"; +import { setIsUploading } from "../store/UploadStatusSlice"; + +const FileUploadModal = ({ show, onClose, onUploadSuccess }) => { + const currentPath = useSelector((state) => state.path.currentPath); + const dispatch = useDispatch(); + const [file, setFile] = useState(null); + const [uploading, setUploading] = useState(false); + const [uploadMessage, setUploadMessage] = useState(""); + const [newFolderName, setNewFolderName] = useState(""); + const [creatingFolder, setCreatingFolder] = useState(false); + const [folderMessage, setFolderMessage] = useState(""); + const username = localStorage.getItem("username"); + const API_URL = import.meta.env.VITE_API_URL; + + useEffect(() => { + const handleEsc = (e) => { + if (e.key === "Escape") onClose(); + }; + document.addEventListener("keydown", handleEsc); + + return () => document.removeEventListener("keydown", handleEsc); + }, [onClose]); + + const isFolderNameValid = (name) => { + return /^[a-zA-Z0-9-_ ]+$/.test(name); // disallow special chars like / \ * ? etc. + }; + + const uploadFileToHDFS = async () => { + if (!file) { + setUploadMessage("⚠️ Please select a file before uploading."); + return; + } + const formData = new FormData(); + + formData.append("file", file); + formData.append("hdfsPath", currentPath); + formData.append("uploadedFileName", file.name); + formData.append("username", username); + + try { + setUploading(true); + setUploadMessage("⏳ Uploading file..."); + const response = await fetch(`${API_URL}/api/hdfs/uploadFile`, { + method: "POST", + body: formData, + }); + if (!response.ok) { + const errorText = await response.text(); + setUploadMessage(`❌ Upload failed: ${errorText}`); + } else { + setUploadMessage("βœ… File uploaded successfully!"); + dispatch(setIsUploading(true)); // Dispatch the action to set isUploading to true + onUploadSuccess(); // Call the onUploadSuccess prop to notify the parent + setTimeout(() => { + setUploadMessage(""); + onClose(); + }, 1000); + } + } catch (err) { + console.error(err); + setUploadMessage("❌ An error occurred during upload."); + } finally { + setUploading(false); + } + }; + + const createFolder = async () => { + if (!newFolderName.trim()) { + setFolderMessage("⚠️ Please enter a folder name."); + return; + } + if (!isFolderNameValid(newFolderName.trim())) { + setFolderMessage("❌ Folder name contains invalid characters."); + return; + } + + try { + setCreatingFolder(true); + setFolderMessage("⏳ Creating folder..."); + const folderPath = + currentPath === "/" ? "" : currentPath.replace(/\/$/, ""); + const newFolderPath = `${folderPath}/${newFolderName}`; + console.log(newFolderPath); + const response = await fetch( + `${API_URL}/api/hdfs/createFolder?hdfsPath=${newFolderPath}`, + { method: "POST" } + ); + + if (!response.ok) { + const errorText = await response.text(); + setFolderMessage(`❌ Folder creation failed: ${errorText}`); + } else { + setFolderMessage("βœ… Folder created successfully!"); + dispatch(setIsUploading(true)); // Dispatch the action to set isUploading to true + onUploadSuccess(currentPath); // Call the onUploadSuccess prop after folder creation too + setNewFolderName(""); + setTimeout(() => { + setFolderMessage(""); + onClose(); + }, 1000); + } + } catch (err) { + console.error(err); + setFolderMessage("❌ An error occurred during folder creation."); + } finally { + setCreatingFolder(false); + } + }; + + if (!show) return null; + + return ( +
+
+
+
+

Manage HDFS

+ +
+
+ {/* File Upload Section */} +
+

+ Upload File +

+ setFile(e.target.files[0])} + className="w-full mb-3 text-sm text-gray-600 file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100" + /> + {file && ( +

+ Selected file: {file.name} +

+ )} + {uploadMessage && ( +

{uploadMessage}

+ )} + +
+ + {/* Create Folder Section */} +
+

+ Create Folder +

+ setNewFolderName(e.target.value)} + className="w-full mb-3 px-3 py-2 border border-gray-300 rounded-lg placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-200" + /> + {folderMessage && ( +

{folderMessage}

+ )} + +
+
+
+
+ ); +}; +FileUploadModal.propTypes = { + show: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + onUploadSuccess: PropTypes.func.isRequired, +}; + +export default FileUploadModal; diff --git a/Frontend/src/components/Footer.jsx b/Frontend/src/components/Footer.jsx index 1150cc0..528c0c5 100644 --- a/Frontend/src/components/Footer.jsx +++ b/Frontend/src/components/Footer.jsx @@ -42,22 +42,38 @@ const Footer = () => { />
-

Drive-thru

+

Skycrate

Your secure cloud storage solution for all your digital needs.

@@ -117,7 +133,8 @@ const Footer = () => {

Stay Updated

- Get exclusive tips, updates on new features, and special offers directly in your inbox. + Get exclusive tips, updates on new features, and special offers + directly in your inbox.

{ {/* Bottom Section */}
-

Β© {new Date().getFullYear()} Drive-Thru. All rights reserved.

+

Β© {new Date().getFullYear()} Skycrate. All rights reserved.

diff --git a/Frontend/src/components/Sidebar.jsx b/Frontend/src/components/Sidebar.jsx index beea1a0..540fe82 100644 --- a/Frontend/src/components/Sidebar.jsx +++ b/Frontend/src/components/Sidebar.jsx @@ -1,136 +1,120 @@ -import { Link } from "react-router-dom"; +import { useState, useEffect, useRef } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { toast } from "react-hot-toast"; const Sidebar = () => { + const navigate = useNavigate(); // Hook for programmatic navigation + const [userMenuOpen, setUserMenuOpen] = useState(false); + const menuRef = useRef(); + + // Show loading toast and perform logout + const handleLogout = () => { + const loadingToast = toast.loading("Logging out..."); + + // Simulate a delay (for example, network request) + setTimeout(() => { + // Remove the token from localStorage + localStorage.removeItem("token"); + localStorage.removeItem("username"); + localStorage.removeItem("expiresIn"); + + // Redirect user to the homepage + navigate("/"); + + // Show success toast after logout + toast.update(loadingToast, { + render: "Logged out successfully!", + type: "success", + isLoading: false, + autoClose: 2000, + }); + }, 1500); + }; + + // Close dropdown on outside click + useEffect(() => { + const handleClickOutside = (e) => { + if (menuRef.current && !menuRef.current.contains(e.target)) { + setUserMenuOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + return ( <> -
- File Name - - Type - - Action - NameActions
+ No files found.
+ + {getIcon(name, type)} {name} {type} - {isFile(entry) ? ( - - Download - - ) : ( + + {isFile(entry) && ( )} +