Compare commits
117 Commits
deployment
..
Ui
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e86beab1f | |||
| a12a53a44a | |||
| 0cd4738d09 | |||
| dff2de5ddd | |||
| 7597f52b47 | |||
| 5283a2b9f1 | |||
|
8f32c77f7e
|
|||
|
2db526d949
|
|||
|
17046735da
|
|||
|
5cd80fe27e
|
|||
|
d2e9f80c30
|
|||
|
43d04c7f93
|
|||
|
1cbd74b6a5
|
|||
|
d88f1b6e0b
|
|||
|
73a1c521d5
|
|||
|
9050bbc5cf
|
|||
|
2cce8d89ca
|
|||
|
41435aa4fc
|
|||
|
7dc8a49a8d
|
|||
|
816d115fbc
|
|||
|
715f3a9d96
|
|||
|
3febc68b4e
|
|||
|
f06967708d
|
|||
|
f79435d64f
|
|||
|
04ac930900
|
|||
|
d8193f8174
|
|||
|
d08b0d6f90
|
|||
|
3b027e4a39
|
|||
|
49f57b5c10
|
|||
|
ac75a64ec8
|
|||
|
0e195ac079
|
|||
|
6358e7e72d
|
|||
|
97be5d1b93
|
|||
|
c42a9dacf0
|
|||
|
4b929bb272
|
|||
|
1189e7cb78
|
|||
|
2f6f0ba747
|
|||
|
96f9ddb1d8
|
|||
| 2c29597f1d | |||
| 1f6cbf4310 | |||
| afccce0be3 | |||
| 96fc18ab80 | |||
| febde7dffe | |||
| c426fecf43 | |||
| 0adc932e53 | |||
| 54dd5a1fcc | |||
| d59e8c789c | |||
| 920c793fa6 | |||
| 608435b758 | |||
| c55dd4b661 | |||
| 5c9e8fedbc | |||
| 6294066ea7 | |||
| 76f9b00624 | |||
| 5391410609 | |||
| a91d7fe8c7 | |||
| 65ca53b224 | |||
| 9d6387699e | |||
| f6371faf9a | |||
| 9632450d16 | |||
| e59784bfa9 | |||
| 6aae767aa6 | |||
| df4d3c1990 | |||
| 4ab49db6af | |||
| 00f6e28207 | |||
| 1f40b02346 | |||
|
8c67d9d4c6
|
|||
|
70f654179a
|
|||
|
a5168a282b
|
|||
| 4219570d80 | |||
| 710a08c868 | |||
| 2f6dcf13f7 | |||
| 8dec47ff63 | |||
| b42a53e99b | |||
|
9e68d73cf4
|
|||
| 2ae2002713 | |||
| 4457823342 | |||
| 1142ece2fd | |||
| 6916142deb | |||
| c798c53dcc | |||
|
596c6bf573
|
|||
|
65783c1ca0
|
|||
|
8c070fd616
|
|||
|
95995d0a4e
|
|||
|
72c1968c67
|
|||
| 000ae78d1e | |||
|
f4ac9a4f15
|
|||
|
7c7d8a6c3a
|
|||
|
0fe8077f7e
|
|||
|
a3ab6731c6
|
|||
| c7697a4b3c | |||
|
7994f2768d
|
|||
|
bb15fe9b7a
|
|||
|
785eb9e66a
|
|||
|
7590e81f5c
|
|||
| 8ba3d6e093 | |||
| 88cd92a159 | |||
| 9c5a000555 | |||
| 130cada092 | |||
| 7f6e377674 | |||
| fe06abadcf | |||
| 00eab70d71 | |||
| 2e74a8b762 | |||
| e3f2d4fd0a | |||
|
ddb39e4258
|
|||
| 6c0b39ddbf | |||
|
b14c132808
|
|||
| a7b5c24d72 | |||
|
2556843ef6
|
|||
| 43f4a8f9ce | |||
| 9abd6c554a | |||
| 1005852091 | |||
|
61a36fc2bc
|
|||
|
7e02ff5ba0
|
|||
|
714ece9637
|
|||
|
b2ba415373
|
|||
|
4f8d15836d
|
|||
|
895872c0bd
|
@@ -1 +0,0 @@
|
||||
environment-variables.txt
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "Backend"]
|
||||
path = Backend
|
||||
url = https://git.kska.io/notkshitij/SkycrateBackend.git
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/Skycrate.iml" filepath="$PROJECT_DIR$/.idea/Skycrate.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="84d52a67-ccf5-4630-9c18-40f188300c16" name="Changes" comment="" />
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo"><![CDATA[{
|
||||
"customColor": "",
|
||||
"associatedIndex": 6
|
||||
}]]></component>
|
||||
<component name="ProjectId" id="30mRMsttilCy5M4kqMOoyd9XSWz" />
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"kotlin-language-version-configured": "true"
|
||||
}
|
||||
}]]></component>
|
||||
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="84d52a67-ccf5-4630-9c18-40f188300c16" name="Changes" comment="" />
|
||||
<created>1754230731355</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1754230731355</updated>
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,29 @@
|
||||
# Info
|
||||
|
||||
---
|
||||
|
||||
## Work distribution
|
||||
|
||||
- Design: Kapil
|
||||
- Frontend: Ombase, Shriniwas, Dinesh, Lalit, Shivani, Pracheta, Vaibhavi
|
||||
- Backend: Vedang, Sonali, Lalit
|
||||
- DBMS: Lalit
|
||||
- HDFS: Sonali, Prajakta, Poonam
|
||||
- Deployment: Kshitij, Sahil
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
In this mini project, we'll be creating something similar to Google Drive. There shall be 3 pages, landing, login/registration and main page where all the files uploaded by the user will be shown. Kapil is supposed to design the UI and send it over by Sunday. Based on this design, people in the frontend department shall work on the pages.
|
||||
|
||||
Landing page is basically a home page containing small description of the project, features etc. Login/registration page will ask for username/password. Lalit is expected to implement it using MySQL/MongoDB, i.e. he is responsible for user authentication. Once the user is authenticated, they shall be redirected to the main page where they can view their files, and upload/delete them.
|
||||
|
||||
Vedang is responsible to developing the backend code in Java for encrypting the uploaded files and decrypting the downloaded files. These files will be stored in Hadoop File System (HDFS) which shall be handled by Sonali, Prajakta and Poonam.
|
||||
|
||||
We are planning to make this a good enough project so that we can maybe open source it and make it a part of our resume. Therefore it is important that y'all work sahi se and finish your stuff by the deadlines. We are expected to finish the entire project ✨ before IN-SEM exam ✨
|
||||
|
||||
While you are working on the project, note down the things you are doing so that we can provide it to the people doing the documentation.
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
VITE_API_URL=http://localhost:8081
|
||||
@@ -0,0 +1,29 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
package-lock.json
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
#########
|
||||
|
||||
.vite/
|
||||
@@ -0,0 +1,28 @@
|
||||
## FRONTEND ##
|
||||
|
||||
# Base image
|
||||
FROM node:22
|
||||
|
||||
# Metadata
|
||||
LABEL maintainer="kshitijka"
|
||||
LABEL version=1.0
|
||||
LABEL description="Skycrate is a web based file management system that uses Hadoop as filesystem."
|
||||
|
||||
# Update & upgrade & rm
|
||||
RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/* && npm install -g http-server
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -s /bin/bash skycrateFront
|
||||
|
||||
# Create work dir
|
||||
RUN mkdir /app
|
||||
RUN chown -R skycrateFront:skycrateFront /app
|
||||
COPY ./dist/ /app
|
||||
WORKDIR /app
|
||||
|
||||
# Switch user
|
||||
USER skycrateFront
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["http-server", "/app"]
|
||||
@@ -0,0 +1,38 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import react from 'eslint-plugin-react'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
settings: { react: { version: '18.3' } },
|
||||
plugins: {
|
||||
react,
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react/jsx-no-target-blank': 'off',
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/image.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="/src/styles.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/flowbite@3.1.2/dist/flowbite.min.css" rel="stylesheet" />
|
||||
|
||||
<title>Skycrate</title>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flowbite@3.1.2/dist/flowbite.min.js"></script>
|
||||
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "Skycrate",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"i18next": "^25.2.1",
|
||||
"lucide-react": "^0.476.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-i18next": "^15.5.3",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"eslint": "^9.29.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^15.15.0",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 317 KiB |
|
After Width: | Height: | Size: 332 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 48 KiB |
@@ -0,0 +1,25 @@
|
||||
import "./App.css";
|
||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||
import LanguageSwitcher from './components/LanguageSwitcher'; // Language switcher dropdown menu
|
||||
import Login from "./pages/Authentication/Login";
|
||||
import SignUp from "./pages/Authentication/SignUp";
|
||||
import DrivethruLandingPage from "./pages/UserPages/DrivethruLandingPage";
|
||||
import Dashboard from "./pages/UserPages/Dashboard";
|
||||
import NotFoundPage from "./pages/UserPages/NotFoundPage";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<LanguageSwitcher />
|
||||
<Routes>
|
||||
<Route path="/" element={<DrivethruLandingPage />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/signup" element={<SignUp />} />
|
||||
<Route path="/Dashboard" element={<Dashboard />} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,237 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setCurrentPath } from "../store/pathSlice";
|
||||
import PasswordForDownload from "./PasswordForDownload";
|
||||
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;
|
||||
|
||||
const FileList = ({ initialPath }) => {
|
||||
const username = localStorage.getItem("username") || "";
|
||||
const userRoot = `/${username}`;
|
||||
|
||||
const [currentPath, setCurrentPathState] = useState(
|
||||
() => initialPath || userRoot
|
||||
);
|
||||
const [files, setFiles] = useState([]);
|
||||
const [showDownloadModal, setShowDownloadModal] = useState(false);
|
||||
const [downloadFilename, setDownloadFilename] = useState("");
|
||||
const dispatch = useDispatch();
|
||||
const isUploading = useSelector((state) => state.upload.isUploading);
|
||||
|
||||
const getType = (entry) =>
|
||||
entry.trim().startsWith("📁") ? "Folder" : "File";
|
||||
const getName = (entry) => entry.trim().replace(/^📁\s*|^📄\s*/, "");
|
||||
const isFile = (entry) => getType(entry) === "File";
|
||||
|
||||
const getIcon = (name, type) => {
|
||||
if (type === "Folder")
|
||||
return <Folder className="text-yellow-500 w-5 h-5 mr-2" />;
|
||||
const ext = name.split(".").pop().toLowerCase();
|
||||
switch (ext) {
|
||||
case "txt":
|
||||
return <FileText className="text-gray-700 w-5 h-5 mr-2" />;
|
||||
case "mp4":
|
||||
case "mkv":
|
||||
return <FileVideo className="text-purple-500 w-5 h-5 mr-2" />;
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
case "png":
|
||||
case "gif":
|
||||
return <FileImage className="text-pink-500 w-5 h-5 mr-2" />;
|
||||
case "mp3":
|
||||
case "wav":
|
||||
return <FileAudio className="text-indigo-500 w-5 h-5 mr-2" />;
|
||||
case "zip":
|
||||
case "rar":
|
||||
case "tar":
|
||||
case "gz":
|
||||
return <FileArchive className="text-red-500 w-5 h-5 mr-2" />;
|
||||
case "csv":
|
||||
case "xls":
|
||||
case "xlsx":
|
||||
return <FileSpreadsheet className="text-green-500 w-5 h-5 mr-2" />;
|
||||
case "ppt":
|
||||
case "pptx":
|
||||
return <Presentation className="text-orange-500 w-5 h-5 mr-2" />;
|
||||
case "js":
|
||||
case "html":
|
||||
case "css":
|
||||
case "java":
|
||||
case "py":
|
||||
case "cpp":
|
||||
return <FileCode2 className="text-blue-500 w-5 h-5 mr-2" />;
|
||||
default:
|
||||
return <FileType2 className="text-gray-500 w-5 h-5 mr-2" />;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchFiles = async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${API_URL}/api/hdfs/listFiles?hdfsPath=${encodeURIComponent(
|
||||
currentPath
|
||||
)}`
|
||||
);
|
||||
const data = await res.json();
|
||||
const filtered = data.filter(
|
||||
(entry) => entry.match(/^ */)[0].length === 0
|
||||
);
|
||||
setFiles(filtered);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch files:", err);
|
||||
setFiles([]);
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
)}`;
|
||||
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}`);
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
if (currentPath === userRoot) return;
|
||||
const parts = currentPath.split("/").filter(Boolean);
|
||||
parts.pop();
|
||||
setCurrentPathState(parts.length === 0 ? userRoot : `/${parts.join("/")}`);
|
||||
};
|
||||
|
||||
// open modal instead of direct download
|
||||
const openDownloadModal = (name, e) => {
|
||||
e.stopPropagation();
|
||||
setDownloadFilename(name);
|
||||
setShowDownloadModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative overflow-x-auto rounded-2xl shadow-lg border border-blue-200">
|
||||
<div className="flex items-center justify-between px-6 py-4 bg-blue-100 text-black font-semibold text-sm">
|
||||
<span className="truncate max-w-[80%]">Path: {currentPath}</span>
|
||||
{currentPath !== userRoot && (
|
||||
<button
|
||||
onClick={goBack}
|
||||
className="flex items-center gap-1 text-blue-600 hover:underline text-sm"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Go Back
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<table className="w-full text-sm text-left text-black">
|
||||
<thead className="text-xs uppercase bg-blue-50 text-blue-800 border-b border-blue-200">
|
||||
<tr>
|
||||
<th className="px-6 py-3">Name</th>
|
||||
<th className="px-6 py-3">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan="2" className="px-6 py-4 text-gray-500 text-center">
|
||||
No files found.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
files.map((entry, idx) => {
|
||||
const name = getName(entry);
|
||||
const type = getType(entry);
|
||||
// const hdfsPath = `${currentPath}/${name}`;
|
||||
return (
|
||||
<tr
|
||||
key={idx}
|
||||
onClick={
|
||||
type === "Folder"
|
||||
? () => 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" : ""
|
||||
}`}
|
||||
>
|
||||
<td className="px-6 py-4 font-medium flex items-center">
|
||||
{getIcon(name, type)}
|
||||
{name}
|
||||
</td>
|
||||
<td className="px-6 py-4 space-x-3">
|
||||
{isFile(entry) && (
|
||||
<button
|
||||
onClick={(e) => openDownloadModal(name, e)}
|
||||
className="text-blue-600 hover:underline inline-flex items-center"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-1" />
|
||||
Download
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => deleteFileOrFolder(name, type, e)}
|
||||
className="text-red-600 hover:underline inline-flex items-center"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{showDownloadModal && (
|
||||
<PasswordForDownload
|
||||
filename={downloadFilename}
|
||||
onDownload={fetchFiles}
|
||||
onClose={() => setShowDownloadModal(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
FileList.propTypes = {
|
||||
initialPath: PropTypes.string,
|
||||
};
|
||||
|
||||
export default FileList;
|
||||
@@ -0,0 +1,194 @@
|
||||
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/files/upload`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
},
|
||||
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 (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black opacity-40" onClick={onClose} />
|
||||
<div className="relative bg-white rounded-2xl shadow-xl w-full max-w-lg mx-4">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b">
|
||||
<h2 className="text-xl font-semibold text-gray-800">Manage HDFS</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-8">
|
||||
{/* File Upload Section */}
|
||||
<div className="bg-gray-50 p-4 rounded-lg shadow-inner">
|
||||
<h3 className="text-lg font-medium text-gray-700 mb-3">
|
||||
Upload File
|
||||
</h3>
|
||||
<input
|
||||
type="file"
|
||||
onChange={(e) => 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 && (
|
||||
<p className="text-sm text-gray-700 mb-2">
|
||||
Selected file: <strong>{file.name}</strong>
|
||||
</p>
|
||||
)}
|
||||
{uploadMessage && (
|
||||
<p className="text-sm text-gray-600 mb-3">{uploadMessage}</p>
|
||||
)}
|
||||
<button
|
||||
onClick={uploadFileToHDFS}
|
||||
disabled={uploading}
|
||||
className="w-full py-2 rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{uploading ? "⏳ Uploading..." : "Upload File"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Create Folder Section */}
|
||||
<div className="bg-gray-50 p-4 rounded-lg shadow-inner">
|
||||
<h3 className="text-lg font-medium text-gray-700 mb-3">
|
||||
Create Folder
|
||||
</h3>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Folder name"
|
||||
value={newFolderName}
|
||||
onChange={(e) => 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 && (
|
||||
<p className="text-sm text-gray-600 mb-3">{folderMessage}</p>
|
||||
)}
|
||||
<button
|
||||
onClick={createFolder}
|
||||
disabled={creatingFolder}
|
||||
className="w-full py-2 rounded-lg bg-green-600 text-white font-medium hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
{creatingFolder ? "⏳ Creating..." : "Create Folder"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
FileUploadModal.propTypes = {
|
||||
show: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onUploadSuccess: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default FileUploadModal;
|
||||
@@ -0,0 +1,189 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Facebook,
|
||||
Twitter,
|
||||
Instagram,
|
||||
Linkedin,
|
||||
Mail,
|
||||
Phone,
|
||||
MapPin,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next"; // for multilinguality
|
||||
|
||||
const Footer = () => {
|
||||
const { t } = useTranslation(); // for multilinguality
|
||||
const [email, setEmail] = useState("");
|
||||
|
||||
//Currently storing user email in localstorage
|
||||
const handleSubscribe = () => {
|
||||
if (email.trim() !== "") {
|
||||
localStorage.setItem("subscribedEmail", email);
|
||||
alert(t("subscribe_success"));
|
||||
setEmail("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<footer className="bg-gradient-to-r from-[#4a7cbd] via-[#5b4fd3] to-[#9377ff] w-full pt-16 pb-8">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 mb-12">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center">
|
||||
<div className="text-white mr-3">
|
||||
<svg
|
||||
className="w-10 h-10"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 2L2 12L12 22L22 12L12 2Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-white">{t("footer_brand")}</h3>
|
||||
</div>
|
||||
<p className="text-white/90">
|
||||
{t("footer_tagline")}
|
||||
</p>
|
||||
<div className="flex space-x-4">
|
||||
<a
|
||||
href="https://facebook.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Facebook className="w-5 h-5 text-white cursor-pointer hover:text-cyan-200 transition-all duration-200 transform hover:scale-110" />
|
||||
</a>
|
||||
<a
|
||||
href="https://twitter.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Twitter className="w-5 h-5 text-white cursor-pointer hover:text-cyan-200 transition-all duration-200 transform hover:scale-110" />
|
||||
</a>
|
||||
<a
|
||||
href="https://instagram.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Instagram className="w-5 h-5 text-white cursor-pointer hover:text-cyan-200 transition-all duration-200 transform hover:scale-110" />
|
||||
</a>
|
||||
<a
|
||||
href="https://linkedin.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Linkedin className="w-5 h-5 text-white cursor-pointer hover:text-cyan-200 transition-all duration-200 transform hover:scale-110" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-white mb-4">{t("footer_quick_links")}</h4>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<a
|
||||
href="#about"
|
||||
className="text-white/90 hover:text-white transition-all duration-200 hover:translate-x-1 inline-block"
|
||||
>
|
||||
{t("footer_about_us")}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#features"
|
||||
className="text-white/90 hover:text-white transition-all duration-200 hover:translate-x-1 inline-block"
|
||||
>
|
||||
{t("footer_features")}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#howItWorks"
|
||||
className="text-white/90 hover:text-white transition-all duration-200 hover:translate-x-1 inline-block"
|
||||
>
|
||||
{t("footer_how_it_works")}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Contact Info */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-white mb-4">{t("footer_contact")}</h4>
|
||||
<ul className="space-y-2">
|
||||
<li className="flex items-center text-white/90 hover:text-white group transition-colors duration-200">
|
||||
<Mail className="w-4 h-4 mr-2 group-hover:text-cyan-200" />
|
||||
{t("footer_email")}
|
||||
</li>
|
||||
<li className="flex items-center text-white/90 hover:text-white group transition-colors duration-200">
|
||||
<Phone className="w-4 h-4 mr-2 group-hover:text-cyan-200" />
|
||||
{t("footer_phone")}
|
||||
</li>
|
||||
<li className="flex items-center text-white/90 hover:text-white group transition-colors duration-200">
|
||||
<MapPin className="w-4 h-4 mr-2 group-hover:text-cyan-200" />
|
||||
{t("footer_address")}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Newsletter */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-white mb-4">{t("footer_newsletter_title")}</h4>
|
||||
<p className="text-white/90 mb-4">
|
||||
{t("footer_newsletter_desc")}
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder={t("footer_newsletter_placeholder")}
|
||||
className="w-full px-4 py-2 rounded-md bg-white/10 border border-white/20 text-white placeholder:text-white/50 focus:bg-white/20 transition-all duration-200 outline-none focus:ring-2 focus:ring-white/30"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSubscribe}
|
||||
className="w-full px-4 py-2 rounded-md bg-white text-blue-600 font-medium hover:bg-opacity-90 transition-all duration-200 transform hover:scale-105"
|
||||
>
|
||||
{t("footer_newsletter_button")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-px w-full bg-white/20 my-8" />
|
||||
|
||||
{/* Bottom Section */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-center text-white/90 text-sm">
|
||||
<p>© {new Date().getFullYear()} {t("footer_brand")}. {t("footer_rights")}</p>
|
||||
<div className="flex gap-4 mt-4 md:mt-0">
|
||||
<a
|
||||
href="#"
|
||||
className="hover:text-white transition-all duration-200 hover:translate-x-1 inline-block"
|
||||
>
|
||||
{t("footer_privacy_policy")}
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
className="hover:text-white transition-all duration-200 hover:translate-x-1 inline-block"
|
||||
>
|
||||
{t("footer_terms_of_service")}
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
className="hover:text-white transition-all duration-200 hover:translate-x-1 inline-block"
|
||||
>
|
||||
{t("footer_cookie_policy")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const languages = [
|
||||
{ code: 'en', label: 'English' },
|
||||
{ code: 'hi', label: 'Hindi (हिंदी)' },
|
||||
{ code: 'mr', label: 'Marathi (मराठी)' },
|
||||
{ code: 'fr', label: 'French (Français)' },
|
||||
// Add more languages as needed
|
||||
];
|
||||
|
||||
function LanguageSwitcher() {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '1rem',
|
||||
right: '1rem',
|
||||
zIndex: 1000,
|
||||
background: 'rgba(255,255,255,0.95)',
|
||||
borderRadius: '4px',
|
||||
padding: '0.25em 0.5em',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.05)'
|
||||
}}
|
||||
>
|
||||
<select
|
||||
value={i18n.language}
|
||||
onChange={e => i18n.changeLanguage(e.target.value)}
|
||||
style={{
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
padding: '0.25em 2em 0.25em 0.5em', // More right padding for arrow
|
||||
minWidth: '100px',
|
||||
appearance: 'auto', // Use browser default arrow
|
||||
background: '#fff',
|
||||
}}
|
||||
|
||||
aria-label="Select language"
|
||||
>
|
||||
{languages.map(lang => (
|
||||
<option key={lang.code} value={lang.code}>
|
||||
{lang.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LanguageSwitcher;
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
const PasswordForDownload = ({ filename, onDownload, onClose }) => {
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!password) {
|
||||
setError("Password is required");
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/files/download`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
},
|
||||
body: JSON.stringify({ filename, password }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const msg = await response.text();
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
onDownload();
|
||||
onClose();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError(`Download failed: ${err.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
|
||||
<div className="bg-white p-6 rounded-lg w-80">
|
||||
<h3 className="text-lg font-semibold mb-4">Enter Password</h3>
|
||||
<input
|
||||
type="password"
|
||||
className="w-full border border-gray-300 rounded px-3 py-2 mb-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
{error && <p className="text-red-500 text-sm mb-2">{error}</p>}
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={loading}
|
||||
className={`px-4 py-2 text-white rounded ${
|
||||
loading ? "bg-gray-400" : "bg-blue-600 hover:bg-blue-700"
|
||||
}`}
|
||||
>
|
||||
{loading ? "Downloading..." : "Download"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PasswordForDownload.propTypes = {
|
||||
filename: PropTypes.string.isRequired,
|
||||
onDownload: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default PasswordForDownload;
|
||||
@@ -0,0 +1,155 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next"; // for multilinguality
|
||||
|
||||
const Sidebar = () => {
|
||||
const { t } = useTranslation(); // for multilinguality
|
||||
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(t("sidebar_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: t("sidebar_logged_out"),
|
||||
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 (
|
||||
<>
|
||||
<nav className="fixed top-0 z-50 h-[60px] w-full bg-white border-b border-gray-200">
|
||||
<div className="p-[15px] h-full lg:px-5 lg:pl-3 flex items-center justify-between">
|
||||
{/* Left Section - Logo & Toggle */}
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
data-drawer-target="logo-sidebar"
|
||||
data-drawer-toggle="logo-sidebar"
|
||||
aria-controls="logo-sidebar"
|
||||
type="button"
|
||||
className="inline-flex items-center p-2 text-lg text-white rounded-lg sm:hidden hover:bg-[#37A0EA] focus:outline-none"
|
||||
>
|
||||
<span className="sr-only">{t("sidebar_open_sidebar")}</span>
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
fillRule="evenodd"
|
||||
d="M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zm0 10.5a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5h-7.5a.75.75 0 01-.75-.75zM2 10a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<Link to="/" className="flex ms-2 md:me-24">
|
||||
<img src="./image.png" className="h-8 me-3" alt="Skycrate Logo" />
|
||||
<span className="self-center text-xl font-semibold sm:text-2xl whitespace-nowrap">
|
||||
{t("sidebar_brand")}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Right Section - Search & User Menu */}
|
||||
<div className="flex items-center">
|
||||
{/* Search Bar */}
|
||||
<div className="flex items-center justify-end mr-40"></div>
|
||||
|
||||
{/* User Profile & Dropdown */}
|
||||
<div className="relative ms-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setUserMenuOpen((o) => !o)}
|
||||
className="flex text-lg bg-gray-800 rounded-full focus:ring-4 focus:ring-gray-300"
|
||||
>
|
||||
<span className="sr-only">{t("sidebar_open_user_menu")}</span>
|
||||
<img
|
||||
className="w-8 h-8 rounded-full"
|
||||
src="https://flowbite.com/docs/images/people/profile-picture-5.jpg"
|
||||
alt={t("sidebar_user_photo")}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{userMenuOpen && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="z-50 absolute right-0 mt-2 w-48 bg-[#1877F2] divide-y divide-gray-100 rounded-sm shadow-sm"
|
||||
>
|
||||
<div className="px-4 py-3" role="none">
|
||||
<p className="text-lg text-white" role="none">
|
||||
{localStorage.getItem("username")}
|
||||
</p>
|
||||
</div>
|
||||
<ul className="py-1" role="none">
|
||||
<li>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full text-left px-4 py-2 text-lg text-white hover:bg-[#37A0EA]"
|
||||
role="menuitem"
|
||||
>
|
||||
{t("sidebar_logout")}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<aside
|
||||
id="logo-sidebar"
|
||||
className="fixed top-0 left-0 z-40 w-64 h-screen pt-[60px] transition-transform -translate-x-full bg-[#1877F2] border-r border-gray-200 sm:translate-x-0"
|
||||
aria-label="Sidebar"
|
||||
>
|
||||
<div className="h-full px-3 pb-4 overflow-y-auto bg-[#1877F2] custom-scrollbar">
|
||||
<ul className="space-y-2 font-medium">
|
||||
<li>
|
||||
<Link
|
||||
to="#"
|
||||
className="flex items-center p-2 mt-5 pt-4 pb-4 text-white rounded-lg hover:bg-[#37A0EA] group"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.62L12 2 9.19 8.62 2 9.24l5.46 4.73L5.82 21z" />
|
||||
</svg>
|
||||
<span className="ms-3">{t("sidebar_starred")}</span>
|
||||
</Link>
|
||||
</li>
|
||||
{/* ...additional sidebar items... */}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
@@ -0,0 +1,30 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import en from './locales/en.json';
|
||||
import hi from './locales/hi.json';
|
||||
import mr from './locales/mr.json';
|
||||
import fr from './locales/fr.json';
|
||||
// import more languages as needed
|
||||
|
||||
const resources = {
|
||||
en: { translation: en },
|
||||
hi: { translation: hi },
|
||||
mr: { translation: mr },
|
||||
fr: { translation: fr },
|
||||
// add other languages here
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
lng: 'en', // default language
|
||||
fallbackLng: 'en',
|
||||
//interpolation: {
|
||||
// escapeValue: false, // not needed for React
|
||||
//},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* For WebKit-based browsers */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
/* or a color of your choice */
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #a0aec0;
|
||||
/* Customize thumb color */
|
||||
border-radius: 4px;
|
||||
border: 2px solid transparent;
|
||||
/* Optional: creates padding around thumb */
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
/* For Firefox */
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: auto;
|
||||
/* "auto" or "thin" */
|
||||
scrollbar-color: #37A0EA transparent;
|
||||
/* thumb and track colors */
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"dashboard": "Dashboard",
|
||||
"failed_to_load_files": "Failed to load files. Please try again later.",
|
||||
|
||||
"skycrate": "Skycrate",
|
||||
"hero_subtitle": "Store, Access & Share Your Files — Anytime, Anywhere!",
|
||||
"hero_desc": "A simple, secure, and fast cloud storage solution for all your files. Upload, organize, and access with ease.",
|
||||
"get_started": "Get Started",
|
||||
"login": "Login",
|
||||
"key_features": "Key Features",
|
||||
"feature_easy_upload_title": "Easy Upload & Access",
|
||||
"feature_easy_upload_desc": "Drag & drop, instant access.",
|
||||
"feature_secure_title": "Secure & Private",
|
||||
"feature_secure_desc": "End-to-end encryption.",
|
||||
"feature_sharing_title": "Seamless Sharing",
|
||||
"feature_sharing_desc": "Share files with one click.",
|
||||
"feature_access_anywhere_title": "Access Anywhere",
|
||||
"feature_access_anywhere_desc": "Works on all devices.",
|
||||
"how_it_works": "How It Works",
|
||||
"how_create_account_title": "Create an account",
|
||||
"how_create_account_desc": "Sign up in seconds.",
|
||||
"how_upload_files_title": "Upload files",
|
||||
"how_upload_files_desc": "Drag & drop or select from your device.",
|
||||
"how_manage_files_title": "Manage files",
|
||||
"how_manage_files_desc": "Rename, move, or delete easily.",
|
||||
"how_access_anytime_title": "Access anytime",
|
||||
"how_access_anytime_desc": "Open files from any device.",
|
||||
|
||||
"not_found_title": "Page Not Found",
|
||||
"not_found_description": "Sorry, we couldn't find the page you were looking for. It may have been moved or deleted.",
|
||||
"go_home": "Go Home",
|
||||
|
||||
"login_title": "Log in",
|
||||
"email_placeholder": "Enter your email",
|
||||
"password_placeholder": "Enter your password",
|
||||
"forgot_password": "Forgot password?",
|
||||
"logging_in": "Logging In...",
|
||||
"login": "Login",
|
||||
"dont_have_account": "Don’t have an account?",
|
||||
"sign_up": "Sign up",
|
||||
"login_successful": "Login successful!",
|
||||
"login_failed": "Login failed.",
|
||||
"an_error_occurred": "An error occurred. Please try again.",
|
||||
"logging_in_toast": "Logging in...",
|
||||
|
||||
"signup_title": "Sign Up",
|
||||
"first_name": "First Name",
|
||||
"last_name": "Last Name",
|
||||
"email_placeholder": "Enter your email",
|
||||
"password_placeholder": "Enter your password",
|
||||
"confirm_password_placeholder": "Confirm your password",
|
||||
"signing_up": "Signing Up...",
|
||||
"sign_up": "Sign Up",
|
||||
"already_have_account": "Already have an account?",
|
||||
"login": "Login",
|
||||
"passwords_do_not_match": "Passwords do not match.",
|
||||
"registering": "Registering...",
|
||||
"signup_failed": "Signup failed.",
|
||||
"folder_creation_failed": "Failed to create user folder.",
|
||||
"signup_success": "Successfully registered and folder created!",
|
||||
"an_error_occurred": "An error occurred. Please try again.",
|
||||
|
||||
"footer_brand": "Skycrate",
|
||||
"footer_tagline": "Your secure cloud storage solution for all your digital needs.",
|
||||
"footer_quick_links": "Quick Links",
|
||||
"footer_about_us": "About Us",
|
||||
"footer_features": "Features",
|
||||
"footer_how_it_works": "How It Works",
|
||||
"footer_contact": "Contact",
|
||||
"footer_email": "support@drivethru.com",
|
||||
"footer_phone": "+91 3628206234",
|
||||
"footer_address": "123 Cloud Street, Digital City",
|
||||
"footer_newsletter_title": "Stay Updated",
|
||||
"footer_newsletter_desc": "Get exclusive tips, updates on new features, and special offers directly in your inbox.",
|
||||
"footer_newsletter_placeholder": "Enter your email",
|
||||
"footer_newsletter_button": "Subscribe to Newsletter",
|
||||
"subscribe_success": "You have successfully subscribed!",
|
||||
"footer_rights": "All rights reserved.",
|
||||
"footer_privacy_policy": "Privacy Policy",
|
||||
"footer_terms_of_service": "Terms of Service",
|
||||
"footer_cookie_policy": "Cookie Policy",
|
||||
|
||||
"sidebar_logging_out": "Logging out...",
|
||||
"sidebar_logged_out": "Logged out successfully!",
|
||||
"sidebar_open_sidebar": "Open sidebar",
|
||||
"sidebar_brand": "Skycrate",
|
||||
"sidebar_open_user_menu": "Open user menu",
|
||||
"sidebar_user_photo": "User Photo",
|
||||
"sidebar_logout": "Log out",
|
||||
"sidebar_starred": "Starred"
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"dashboard": "Tableau de bord",
|
||||
"failed_to_load_files": "Échec du chargement des fichiers. Veuillez réessayer plus tard.",
|
||||
|
||||
"skycrate": "Skycrate",
|
||||
"hero_subtitle": "Stockez, accédez et partagez vos fichiers — à tout moment, partout !",
|
||||
"hero_desc": "Une solution de stockage cloud simple, sécurisée et rapide pour tous vos fichiers. Téléchargez, organisez et accédez facilement.",
|
||||
"get_started": "Commencer",
|
||||
"login": "Connexion",
|
||||
"key_features": "Fonctionnalités clés",
|
||||
"feature_easy_upload_title": "Téléversement et accès faciles",
|
||||
"feature_easy_upload_desc": "Glissez-déposez, accès instantané.",
|
||||
"feature_secure_title": "Sécurisé et privé",
|
||||
"feature_secure_desc": "Chiffrement de bout en bout.",
|
||||
"feature_sharing_title": "Partage sans effort",
|
||||
"feature_sharing_desc": "Partagez des fichiers en un clic.",
|
||||
"feature_access_anywhere_title": "Accès partout",
|
||||
"feature_access_anywhere_desc": "Fonctionne sur tous les appareils.",
|
||||
"how_it_works": "Comment ça marche",
|
||||
"how_create_account_title": "Créer un compte",
|
||||
"how_create_account_desc": "Inscrivez-vous en quelques secondes.",
|
||||
"how_upload_files_title": "Téléverser des fichiers",
|
||||
"how_upload_files_desc": "Glissez-déposez ou sélectionnez depuis votre appareil.",
|
||||
"how_manage_files_title": "Gérer les fichiers",
|
||||
"how_manage_files_desc": "Renommez, déplacez ou supprimez facilement.",
|
||||
"how_access_anytime_title": "Accès à tout moment",
|
||||
"how_access_anytime_desc": "Ouvrez des fichiers depuis n'importe quel appareil.",
|
||||
|
||||
"not_found_title": "Page non trouvée",
|
||||
"not_found_description": "Désolé, nous n'avons pas pu trouver la page que vous cherchiez. Elle a peut-être été déplacée ou supprimée.",
|
||||
"go_home": "Accueil",
|
||||
|
||||
"login_title": "Connexion",
|
||||
"email_placeholder": "Entrez votre e-mail",
|
||||
"password_placeholder": "Entrez votre mot de passe",
|
||||
"forgot_password": "Mot de passe oublié ?",
|
||||
"logging_in": "Connexion...",
|
||||
"login": "Connexion",
|
||||
"dont_have_account": "Vous n'avez pas de compte ?",
|
||||
"sign_up": "S'inscrire",
|
||||
"login_successful": "Connexion réussie !",
|
||||
"login_failed": "Échec de la connexion.",
|
||||
"an_error_occurred": "Une erreur s'est produite. Veuillez réessayer.",
|
||||
"logging_in_toast": "Connexion en cours...",
|
||||
|
||||
"sign_up": "S'inscrire",
|
||||
"first_name": "Prénom",
|
||||
"last_name": "Nom de famille",
|
||||
"email_placeholder": "Entrez votre e-mail",
|
||||
"password_placeholder": "Entrez votre mot de passe",
|
||||
"confirm_password_placeholder": "Confirmez votre mot de passe",
|
||||
"already_have_account": "Vous avez déjà un compte ?",
|
||||
"login": "Connexion",
|
||||
"signing_up": "Inscription...",
|
||||
"passwords_do_not_match": "Les mots de passe ne correspondent pas.",
|
||||
"registering": "Enregistrement...",
|
||||
"signup_failed": "Échec de l'inscription.",
|
||||
"failed_create_folder": "Échec de la création du dossier utilisateur.",
|
||||
"signup_success": "Inscription réussie et dossier créé !",
|
||||
"an_error_occurred": "Une erreur s'est produite. Veuillez réessayer.",
|
||||
|
||||
"footer_brand": "Skycrate",
|
||||
"footer_tagline": "Votre solution de stockage cloud sécurisée pour tous vos besoins numériques.",
|
||||
"footer_quick_links": "Liens rapides",
|
||||
"footer_about_us": "À propos",
|
||||
"footer_features": "Fonctionnalités",
|
||||
"footer_how_it_works": "Comment ça marche",
|
||||
"footer_contact": "Contact",
|
||||
"footer_email": "support@drivethru.com",
|
||||
"footer_phone": "+91 3628206234",
|
||||
"footer_address": "123 Rue du Cloud, Ville Digitale",
|
||||
"footer_newsletter_title": "Restez informé",
|
||||
"footer_newsletter_desc": "Recevez des conseils exclusifs, des mises à jour sur les nouvelles fonctionnalités et des offres spéciales directement dans votre boîte de réception.",
|
||||
"footer_newsletter_placeholder": "Entrez votre e-mail",
|
||||
"footer_newsletter_button": "S'abonner à la newsletter",
|
||||
"subscribe_success": "Vous vous êtes abonné avec succès !",
|
||||
"footer_rights": "Tous droits réservés.",
|
||||
"footer_privacy_policy": "Politique de confidentialité",
|
||||
"footer_terms_of_service": "Conditions d'utilisation",
|
||||
"footer_cookie_policy": "Politique relative aux cookies",
|
||||
|
||||
"sidebar_logging_out": "Déconnexion...",
|
||||
"sidebar_logged_out": "Déconnecté avec succès !",
|
||||
"sidebar_open_sidebar": "Ouvrir la barre latérale",
|
||||
"sidebar_brand": "Skycrate",
|
||||
"sidebar_open_user_menu": "Ouvrir le menu utilisateur",
|
||||
"sidebar_user_photo": "Photo de l'utilisateur",
|
||||
"sidebar_logout": "Se déconnecter",
|
||||
"sidebar_starred": "Favoris"
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"dashboard": "डैशबोर्ड",
|
||||
"failed_to_load_files": "फ़ाइलें लोड करने में विफल। कृपया बाद में पुनः प्रयास करें।",
|
||||
|
||||
"skycrate": "Skycrate",
|
||||
"hero_subtitle": "अपनी फ़ाइलें संग्रहित करें, एक्सेस करें और साझा करें — कभी भी, कहीं भी!",
|
||||
"hero_desc": "आपकी सभी फ़ाइलों के लिए एक सरल, सुरक्षित और तेज़ क्लाउड स्टोरेज समाधान। अपलोड करें, व्यवस्थित करें और आसानी से एक्सेस करें।",
|
||||
"get_started": "शुरू करें",
|
||||
"login": "लॉगिन",
|
||||
"key_features": "मुख्य विशेषताएं",
|
||||
"feature_easy_upload_title": "सरल अपलोड और एक्सेस",
|
||||
"feature_easy_upload_desc": "ड्रैग और ड्रॉप करें, त्वरित एक्सेस पाएं।",
|
||||
"feature_secure_title": "सुरक्षित और निजी",
|
||||
"feature_secure_desc": "एंड-टू-एंड एन्क्रिप्शन।",
|
||||
"feature_sharing_title": "बिना रुकावट साझाकरण",
|
||||
"feature_sharing_desc": "एक क्लिक में फ़ाइलें साझा करें।",
|
||||
"feature_access_anywhere_title": "कहीं से भी एक्सेस करें",
|
||||
"feature_access_anywhere_desc": "सभी डिवाइस पर कार्य करता है।",
|
||||
|
||||
"how_it_works": "यह कैसे कार्य करता है",
|
||||
"how_create_account_title": "खाता बनाएं",
|
||||
"how_create_account_desc": "कुछ ही सेकंड में साइन अप करें।",
|
||||
"how_upload_files_title": "फ़ाइलें अपलोड करें",
|
||||
"how_upload_files_desc": "ड्रैग और ड्रॉप करें या डिवाइस से चुनें।",
|
||||
"how_manage_files_title": "फ़ाइलें प्रबंधित करें",
|
||||
"how_manage_files_desc": "आसानी से नाम बदलें, स्थानांतरित करें या हटाएं।",
|
||||
"how_access_anytime_title": "कभी भी एक्सेस करें",
|
||||
"how_access_anytime_desc": "किसी भी डिवाइस से फ़ाइलें खोलें।",
|
||||
|
||||
"not_found_title": "पृष्ठ नहीं मिला",
|
||||
"not_found_description": "क्षमा करें, हम वह पृष्ठ नहीं ढूंढ सके जिसे आप खोज रहे थे। यह हटाया गया हो सकता है या स्थानांतरित कर दिया गया हो।",
|
||||
"go_home": "मुख्य पृष्ठ पर जाएं",
|
||||
|
||||
"login_title": "लॉग इन करें",
|
||||
"email_placeholder": "अपना ईमेल दर्ज करें",
|
||||
"password_placeholder": "अपना पासवर्ड दर्ज करें",
|
||||
"forgot_password": "पासवर्ड भूल गए?",
|
||||
"logging_in": "लॉग इन किया जा रहा है...",
|
||||
"login": "लॉगिन",
|
||||
"dont_have_account": "कोई खाता नहीं है?",
|
||||
"sign_up": "साइन अप करें",
|
||||
"login_successful": "सफलतापूर्वक लॉगिन हुआ!",
|
||||
"login_failed": "लॉगिन विफल रहा।",
|
||||
"an_error_occurred": "एक त्रुटि हुई। कृपया पुनः प्रयास करें।",
|
||||
"logging_in_toast": "लॉग इन किया जा रहा है...",
|
||||
|
||||
"signup_title": "साइन अप करें",
|
||||
"first_name": "पहला नाम",
|
||||
"last_name": "अंतिम नाम",
|
||||
"email_placeholder": "अपना ईमेल दर्ज करें",
|
||||
"password_placeholder": "अपना पासवर्ड दर्ज करें",
|
||||
"confirm_password_placeholder": "अपना पासवर्ड पुष्टि करें",
|
||||
"signing_up": "साइन अप किया जा रहा है...",
|
||||
"sign_up": "साइन अप करें",
|
||||
"already_have_account": "पहले से ही खाता है?",
|
||||
"login": "लॉगिन",
|
||||
"passwords_do_not_match": "पासवर्ड मेल नहीं खा रहे हैं।",
|
||||
"registering": "पंजीकरण किया जा रहा है...",
|
||||
"signup_failed": "साइन अप विफल रहा।",
|
||||
"folder_creation_failed": "यूज़र फ़ोल्डर बनाने में विफल।",
|
||||
"signup_success": "सफलतापूर्वक पंजीकरण हुआ और फ़ोल्डर बनाया गया!",
|
||||
"an_error_occurred": "एक त्रुटि हुई। कृपया पुनः प्रयास करें।",
|
||||
|
||||
"footer_brand": "Skycrate",
|
||||
"footer_tagline": "आपकी सभी डिजिटल आवश्यकताओं के लिए सुरक्षित क्लाउड स्टोरेज समाधान।",
|
||||
"footer_quick_links": "त्वरित लिंक",
|
||||
"footer_about_us": "हमारे बारे में",
|
||||
"footer_features": "विशेषताएं",
|
||||
"footer_how_it_works": "यह कैसे कार्य करता है",
|
||||
"footer_contact": "संपर्क करें",
|
||||
"footer_email": "support@drivethru.com",
|
||||
"footer_phone": "+91 3628206234",
|
||||
"footer_address": "123 क्लाउड स्ट्रीट, डिजिटल सिटी",
|
||||
"footer_newsletter_title": "अपडेट प्राप्त करें",
|
||||
"footer_newsletter_desc": "विशेष सुझाव, नई सुविधाओं के अपडेट और ऑफ़र सीधे अपने इनबॉक्स में पाएं।",
|
||||
"footer_newsletter_placeholder": "अपना ईमेल दर्ज करें",
|
||||
"footer_newsletter_button": "न्यूज़लेटर की सदस्यता लें",
|
||||
"subscribe_success": "आपने सफलतापूर्वक सदस्यता ली है!",
|
||||
"footer_rights": "सभी अधिकार सुरक्षित।",
|
||||
"footer_privacy_policy": "गोपनीयता नीति",
|
||||
"footer_terms_of_service": "सेवा की शर्तें",
|
||||
"footer_cookie_policy": "कुकी नीति",
|
||||
|
||||
"sidebar_logging_out": "लॉग आउट किया जा रहा है...",
|
||||
"sidebar_logged_out": "सफलतापूर्वक लॉग आउट हुआ!",
|
||||
"sidebar_open_sidebar": "साइडबार खोलें",
|
||||
"sidebar_brand": "Skycrate",
|
||||
"sidebar_open_user_menu": "उपयोगकर्ता मेनू खोलें",
|
||||
"sidebar_user_photo": "उपयोगकर्ता फोटो",
|
||||
"sidebar_logout": "लॉग आउट",
|
||||
"sidebar_starred": "चिह्नित"
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"dashboard": "डॅशबोर्ड",
|
||||
"failed_to_load_files": "फायली लोड करण्यात अयशस्वी. कृपया नंतर पुन्हा प्रयत्न करा.",
|
||||
|
||||
"skycrate": "Skycrate",
|
||||
"hero_subtitle": "तुमच्या फायली साठवा, प्रवेश मिळवा आणि शेअर करा — कधीही, कुठेही!",
|
||||
"hero_desc": "सर्व फायलींसाठी एक साधे, सुरक्षित आणि जलद क्लाऊड स्टोरेज सोल्यूशन. अपलोड करा, व्यवस्थापित करा आणि सहजपणे वापरा.",
|
||||
"get_started": "सुरुवात करा",
|
||||
"login": "लॉगिन",
|
||||
"key_features": "मुख्य वैशिष्ट्ये",
|
||||
"feature_easy_upload_title": "सोपे अपलोड आणि प्रवेश",
|
||||
"feature_easy_upload_desc": "ड्रॅग आणि ड्रॉप, त्वरित प्रवेश.",
|
||||
"feature_secure_title": "सुरक्षित आणि खाजगी",
|
||||
"feature_secure_desc": "संपूर्ण एन्क्रिप्शन.",
|
||||
"feature_sharing_title": "सुलभ शेअरिंग",
|
||||
"feature_sharing_desc": "एक क्लिकमध्ये फायली शेअर करा.",
|
||||
"feature_access_anywhere_title": "कोठूनही प्रवेश",
|
||||
"feature_access_anywhere_desc": "सर्व डिव्हाइसेसवर कार्यरत.",
|
||||
|
||||
"how_it_works": "हे कसे कार्य करते",
|
||||
"how_create_account_title": "खाते तयार करा",
|
||||
"how_create_account_desc": "काही सेकंदांत साइन अप करा.",
|
||||
"how_upload_files_title": "फायली अपलोड करा",
|
||||
"how_upload_files_desc": "ड्रॅग आणि ड्रॉप करा किंवा तुमच्या डिव्हाइसमधून निवडा.",
|
||||
"how_manage_files_title": "फायली व्यवस्थापित करा",
|
||||
"how_manage_files_desc": "नाव बदला, हलवा किंवा हटवा.",
|
||||
"how_access_anytime_title": "कधीही प्रवेश करा",
|
||||
"how_access_anytime_desc": "कोणत्याही डिव्हाइसवरून फायली उघडा.",
|
||||
|
||||
"not_found_title": "पृष्ठ सापडले नाही",
|
||||
"not_found_description": "क्षमस्व, तुम्ही शोधत असलेले पृष्ठ आम्हाला सापडले नाही. कदाचित ते हलवले गेले असेल किंवा हटवले गेले असेल.",
|
||||
"go_home": "मुख्य पृष्ठावर जा",
|
||||
|
||||
"login_title": "लॉग इन करा",
|
||||
"email_placeholder": "तुमचा ईमेल टाका",
|
||||
"password_placeholder": "तुमचा पासवर्ड टाका",
|
||||
"forgot_password": "पासवर्ड विसरलात?",
|
||||
"logging_in": "लॉग इन करत आहे...",
|
||||
"login": "लॉग इन",
|
||||
"dont_have_account": "अजून खाते नाही?",
|
||||
"sign_up": "साइन अप",
|
||||
"login_successful": "यशस्वीरित्या लॉग इन झाले!",
|
||||
"login_failed": "लॉग इन अयशस्वी.",
|
||||
"an_error_occurred": "त्रुटी आली. कृपया पुन्हा प्रयत्न करा.",
|
||||
"logging_in_toast": "लॉग इन होत आहे...",
|
||||
|
||||
"signup_title": "साइन अप करा",
|
||||
"first_name": "पहिले नाव",
|
||||
"last_name": "आडनाव",
|
||||
"email_placeholder": "तुमचा ईमेल टाका",
|
||||
"password_placeholder": "तुमचा पासवर्ड टाका",
|
||||
"confirm_password_placeholder": "तुमचा पासवर्ड पुन्हा टाका",
|
||||
"signing_up": "साइन अप करत आहे...",
|
||||
"sign_up": "साइन अप",
|
||||
"already_have_account": "आधीच खाते आहे?",
|
||||
"login": "लॉग इन",
|
||||
"passwords_do_not_match": "पासवर्ड जुळत नाहीत.",
|
||||
"registering": "नोंदणी करत आहे...",
|
||||
"signup_failed": "साइन अप अयशस्वी.",
|
||||
"folder_creation_failed": "वापरकर्त्याची फोल्डर तयार करण्यात अयशस्वी.",
|
||||
"signup_success": "यशस्वीरित्या नोंदणी झाली आणि फोल्डर तयार झाला!",
|
||||
"an_error_occurred": "त्रुटी आली. कृपया पुन्हा प्रयत्न करा.",
|
||||
|
||||
"footer_brand": "Skycrate",
|
||||
"footer_tagline": "तुमच्या सर्व डिजिटल गरजांसाठी सुरक्षित क्लाऊड स्टोरेज सोल्यूशन.",
|
||||
"footer_quick_links": "त्वरित दुवे",
|
||||
"footer_about_us": "आमच्याबद्दल",
|
||||
"footer_features": "वैशिष्ट्ये",
|
||||
"footer_how_it_works": "हे कसे कार्य करते",
|
||||
"footer_contact": "संपर्क",
|
||||
"footer_email": "support@drivethru.com",
|
||||
"footer_phone": "+९१ ३६२८२०६२३४",
|
||||
"footer_address": "१२३ क्लाऊड स्ट्रीट, डिजिटल सिटी",
|
||||
"footer_newsletter_title": "अपडेट मिळवा",
|
||||
"footer_newsletter_desc": "विशेष टिप्स, नवीन वैशिष्ट्यांवरील अपडेट्स आणि खास ऑफर्स तुमच्या इनबॉक्समध्ये मिळवा.",
|
||||
"footer_newsletter_placeholder": "तुमचा ईमेल टाका",
|
||||
"footer_newsletter_button": "न्यूजलेटरची सदस्यता घ्या",
|
||||
"subscribe_success": "तुमची सदस्यता यशस्वीरित्या घेतली गेली आहे!",
|
||||
"footer_rights": "सर्व हक्क राखीव.",
|
||||
"footer_privacy_policy": "गोपनीयता धोरण",
|
||||
"footer_terms_of_service": "सेवेच्या अटी",
|
||||
"footer_cookie_policy": "कुकी धोरण",
|
||||
|
||||
"sidebar_logging_out": "लॉग आउट करत आहे...",
|
||||
"sidebar_logged_out": "यशस्वीरित्या लॉग आउट झाले!",
|
||||
"sidebar_open_sidebar": "साइडबार उघडा",
|
||||
"sidebar_brand": "Skycrate",
|
||||
"sidebar_open_user_menu": "वापरकर्ता मेनू उघडा",
|
||||
"sidebar_user_photo": "वापरकर्त्याचा फोटो",
|
||||
"sidebar_logout": "लॉग आउट",
|
||||
"sidebar_starred": "आवडते"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import './i18n'; // for multilingual functionality
|
||||
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App.jsx";
|
||||
import { Provider } from "react-redux";
|
||||
import { store } from "./store/store";
|
||||
|
||||
const container = document.getElementById("root");
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
</StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,187 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { FiEye, FiEyeOff, FiLoader } from "react-icons/fi";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import toast, { Toaster } from "react-hot-toast"; // Import React Hot Toast
|
||||
import { useTranslation } from "react-i18next"; // for multilinguality
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL; // Using .env variable
|
||||
|
||||
const Login = () => {
|
||||
const { t } = useTranslation(); // for multilinguality
|
||||
const navigate = useNavigate(); // For navigation
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
// Redirect if already logged in
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem("token")) {
|
||||
navigate("/dashboard");
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
const togglePassword = () => setShowPassword((prev) => !prev);
|
||||
|
||||
const validate = () => {
|
||||
const errs = {};
|
||||
if (!email.trim()) errs.email = t("email_required");
|
||||
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
|
||||
errs.email = t("invalid_email");
|
||||
if (!password) errs.password = t("password_required");
|
||||
return errs;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const validation = validate();
|
||||
if (Object.keys(validation).length) {
|
||||
setErrors(validation);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const toastId = toast.loading(t("logging_in_toast"));
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
const data = await response.json();
|
||||
toast.dismiss(toastId);
|
||||
|
||||
if (response.ok) {
|
||||
localStorage.setItem("token", data.token);
|
||||
localStorage.setItem("expiresIn", data.expiresIn);
|
||||
// fetch username asynchronously
|
||||
fetch(`${API_URL}/api/hdfs/getUsernameByEmail?email=${email}`)
|
||||
.then((res) => res.text())
|
||||
.then((username) => localStorage.setItem("username", username))
|
||||
.catch((err) => console.error("Error fetching username:", err));
|
||||
|
||||
toast.success(t("login_successful"));
|
||||
navigate("/dashboard");
|
||||
} else {
|
||||
toast.error(data.message || t("login_failed"));
|
||||
}
|
||||
} catch (error) {
|
||||
toast.dismiss(toastId);
|
||||
console.error(error);
|
||||
toast.error(t("an_error_occurred"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||
<Toaster position="top-right" />
|
||||
<div className="w-full max-w-sm bg-white rounded-2xl shadow-lg p-8">
|
||||
<h1 className="text-2xl font-bold text-gray-800 mb-6 text-center">
|
||||
{t("login_title")}
|
||||
</h1>
|
||||
<form onSubmit={handleSubmit} noValidate className="space-y-5">
|
||||
{/* Email Field */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{t("email_placeholder")}
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
value={email}
|
||||
onChange={(e) => {
|
||||
setEmail(e.target.value);
|
||||
setErrors((prev) => ({ ...prev, email: undefined }));
|
||||
}}
|
||||
className={`w-full border ${
|
||||
errors.email ? "border-red-500" : "border-gray-300"
|
||||
} rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500`}
|
||||
placeholder={t("email_placeholder")}
|
||||
required
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-red-500 text-xs mt-1">{errors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password Field */}
|
||||
<div className="relative">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{t("password_placeholder")}
|
||||
</label>
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => {
|
||||
setPassword(e.target.value);
|
||||
setErrors((prev) => ({ ...prev, password: undefined }));
|
||||
}}
|
||||
className={`w-full border ${
|
||||
errors.password ? "border-red-500" : "border-gray-300"
|
||||
} rounded-lg px-4 py-2 pr-10 focus:outline-none focus:ring-2 focus:ring-blue-500`}
|
||||
placeholder={t("password_placeholder")}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={togglePassword}
|
||||
className="absolute right-3 top-8 text-xl text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
{showPassword ? <FiEyeOff /> : <FiEye />}
|
||||
</button>
|
||||
{errors.password && (
|
||||
<p className="text-red-500 text-xs mt-1">{errors.password}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Forgot & Submit */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Link to="#!" className="text-sm text-blue-600 hover:underline">
|
||||
{t("forgot_password")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className={`w-full flex justify-center items-center py-3 ${
|
||||
loading
|
||||
? "bg-gray-400 cursor-not-allowed"
|
||||
: "bg-gradient-to-r from-blue-600 to-blue-800 hover:from-blue-700 hover:to-blue-900"
|
||||
} text-white font-semibold rounded-lg shadow-md transition duration-300`}
|
||||
>
|
||||
{loading ? (
|
||||
<FiLoader className="animate-spin text-lg" />
|
||||
) : (
|
||||
t("login")
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="text-center mt-6 text-gray-600">
|
||||
{t("dont_have_account")}{" "}
|
||||
<Link
|
||||
to="/signup"
|
||||
className="text-green-600 hover:underline font-medium"
|
||||
>
|
||||
{t("sign_up")}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -0,0 +1,253 @@
|
||||
import { useState } from "react";
|
||||
import { FiEye, FiEyeOff, FiLoader } from "react-icons/fi";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import toast, { Toaster } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next"; // for multilinguality
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
const SignUp = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
firstname: "",
|
||||
lastname: "",
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const validate = () => {
|
||||
const errs = {};
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email))
|
||||
errs.email = t("invalid_email");
|
||||
if (formData.password.length < 8) errs.password = t("password_too_short");
|
||||
if (formData.password !== formData.confirmPassword)
|
||||
errs.confirmPassword = t("passwords_do_not_match");
|
||||
if (formData.username.length < 3) errs.username = t("username_too_short");
|
||||
return errs;
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
setErrors((prev) => ({ ...prev, [name]: undefined }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const validation = validate();
|
||||
if (Object.keys(validation).length) {
|
||||
setErrors(validation);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const toastId = toast.loading(t("registering"));
|
||||
|
||||
try {
|
||||
const signupRes = await fetch(`${API_URL}/api/auth/register`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
firstname: formData.firstname,
|
||||
lastname: formData.lastname,
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
username: formData.username,
|
||||
fullname: `${formData.firstname} ${formData.lastname}`,
|
||||
}),
|
||||
});
|
||||
const signupData = await signupRes.json();
|
||||
|
||||
if (!signupRes.ok) {
|
||||
toast.error(signupData.message || t("signup_failed"), { id: toastId });
|
||||
return;
|
||||
}
|
||||
|
||||
const folderRes = await fetch(
|
||||
`${API_URL}/api/hdfs/createFolder?hdfsPath=/${formData.username}`,
|
||||
{ method: "POST" }
|
||||
);
|
||||
|
||||
if (!folderRes.ok) {
|
||||
toast.error(t("failed_create_folder"), { id: toastId });
|
||||
} else {
|
||||
toast.success(t("signup_success"), { id: toastId });
|
||||
}
|
||||
|
||||
setTimeout(() => navigate("/login"), 1500);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error(t("an_error_occurred"), { id: toastId });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 flex items-center justify-center p-6">
|
||||
<Toaster position="top-right" />
|
||||
<div className="w-full max-w-md bg-white rounded-2xl shadow-lg p-8">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-6">
|
||||
{t("sign_up")}
|
||||
</h1>
|
||||
<form className="space-y-4" onSubmit={handleSubmit} noValidate>
|
||||
{/* Name Fields */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{t("first_name")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="firstname"
|
||||
value={formData.firstname}
|
||||
onChange={handleChange}
|
||||
className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{t("last_name")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="lastname"
|
||||
value={formData.lastname}
|
||||
onChange={handleChange}
|
||||
className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Username Field */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{t("username")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
placeholder={t("Enter your username")}
|
||||
value={formData.username}
|
||||
onChange={handleChange}
|
||||
className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
{errors.username && (
|
||||
<p className="text-red-500 text-xs mt-1">{errors.username}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Email Field */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{t("email_placeholder")}
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-red-500 text-xs mt-1">{errors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password Field */}
|
||||
<div className="relative">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{t("password_placeholder")}
|
||||
</label>
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
className="w-full border border-gray-300 rounded-lg px-4 py-2 pr-10 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword((v) => !v)}
|
||||
className="absolute right-3 top-9 text-xl text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
{showPassword ? <FiEyeOff /> : <FiEye />}
|
||||
</button>
|
||||
{errors.password && (
|
||||
<p className="text-red-500 text-xs mt-1">{errors.password}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Confirm Password Field */}
|
||||
<div className="relative">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{t("confirm_password_placeholder")}
|
||||
</label>
|
||||
<input
|
||||
type={showConfirmPassword ? "text" : "password"}
|
||||
name="confirmPassword"
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleChange}
|
||||
className="w-full border border-gray-300 rounded-lg px-4 py-2 pr-10 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowConfirmPassword((v) => !v)}
|
||||
className="absolute right-3 top-9 text-xl text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
{showConfirmPassword ? <FiEyeOff /> : <FiEye />}
|
||||
</button>
|
||||
{errors.confirmPassword && (
|
||||
<p className="text-red-500 text-xs mt-1">
|
||||
{errors.confirmPassword}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sign Up Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className={`w-full mt-4 py-3 flex justify-center items-center ${
|
||||
loading
|
||||
? "bg-gray-400 cursor-not-allowed"
|
||||
: "bg-gradient-to-r from-[#10B981] to-[#07533A] hover:from-[#0E458C] hover:to-[#1877F2]"
|
||||
} text-white font-semibold rounded-lg shadow-md transition duration-300`}
|
||||
>
|
||||
{loading ? (
|
||||
<FiLoader className="animate-spin text-xl" />
|
||||
) : (
|
||||
t("sign_up")
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="text-center mt-4 text-gray-700">
|
||||
{t("already_have_account")}{" "}
|
||||
<Link
|
||||
to="/login"
|
||||
className="text-blue-500 hover:underline font-medium"
|
||||
>
|
||||
{t("login")}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignUp;
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next"; // for multilinguality
|
||||
import Sidebar from "../../components/Sidebar";
|
||||
import FileList from "../../components/FileList";
|
||||
import FileUploadModal from "../../components/FileUploadModal";
|
||||
import { FiPlus } from "react-icons/fi";
|
||||
|
||||
const Dashboard = () => {
|
||||
const { t } = useTranslation(); // for multilinguality
|
||||
const [files, setFiles] = useState([]);
|
||||
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const navigate = useNavigate();
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
const isUserLoggedIn = () => {
|
||||
const token = localStorage.getItem("token");
|
||||
const username = localStorage.getItem("username");
|
||||
const expiresIn = localStorage.getItem("expiresIn");
|
||||
|
||||
if (!token || !username || !expiresIn) return false;
|
||||
|
||||
const expiryTime = new Date(expiresIn).getTime();
|
||||
const now = new Date().getTime();
|
||||
|
||||
if (now > expiryTime) {
|
||||
localStorage.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const fetchFiles = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/hdfs/listFiles?hdfsPath=/`);
|
||||
const data = await response.json();
|
||||
setFiles(data);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch files:", error);
|
||||
setError(t("failed_to_load_files"));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isUserLoggedIn()) {
|
||||
navigate("/login");
|
||||
} else {
|
||||
fetchFiles();
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [navigate]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sidebar />
|
||||
<div className="p-4 sm:ml-64">
|
||||
<div className="p-4 border-2 border-gray-200 border-dashed rounded-lg mt-14">
|
||||
<div className="w-full flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold mb-4">{t("dashboard")}</h1>
|
||||
<button
|
||||
onClick={() => setIsUploadModalOpen(true)}
|
||||
className="block text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-3 py-2 text-center"
|
||||
type="button"
|
||||
>
|
||||
<FiPlus className="text-2xl" />
|
||||
</button>
|
||||
</div>
|
||||
{error ? (
|
||||
<p className="text-red-500">{error}</p>
|
||||
) : (
|
||||
<FileList files={files} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FileUploadModal
|
||||
show={isUploadModalOpen}
|
||||
onClose={() => setIsUploadModalOpen(false)}
|
||||
onUploadSuccess={() => {
|
||||
fetchFiles();
|
||||
setIsUploadModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -0,0 +1,378 @@
|
||||
import Footer from "../../components/Footer";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next"; // for multilinguality
|
||||
|
||||
const DrivethruLandingPage = () => {
|
||||
const { t } = useTranslation(); // for multilinguality
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: t("feature_easy_upload_title"),
|
||||
description: t("feature_easy_upload_desc"),
|
||||
icon: (
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("feature_secure_title"),
|
||||
description: t("feature_secure_desc"),
|
||||
icon: (
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("feature_sharing_title"),
|
||||
description: t("feature_sharing_desc"),
|
||||
icon: (
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("feature_access_anywhere_title"),
|
||||
description: t("feature_access_anywhere_desc"),
|
||||
icon: (
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h.5A2.5 2.5 0 0020 5.5v-1.65"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const howItWorks = [
|
||||
{
|
||||
title: t("how_create_account_title"),
|
||||
description: t("how_create_account_desc"),
|
||||
icon: (
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("how_upload_files_title"),
|
||||
description: t("how_upload_files_desc"),
|
||||
icon: (
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("how_manage_files_title"),
|
||||
description: t("how_manage_files_desc"),
|
||||
icon: (
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("how_access_anytime_title"),
|
||||
description: t("how_access_anytime_desc"),
|
||||
icon: (
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
// UseEffect and handle....click function to handle set and handle the animation of features..
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPaused) {
|
||||
const interval = setInterval(() => {
|
||||
setActiveIndex((prevIndex) => (prevIndex + 1) % features.length);
|
||||
}, 3000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [isPaused, features.length]);
|
||||
|
||||
// Handle user interaction
|
||||
const handleFeatureClick = (index) => {
|
||||
setActiveIndex(index);
|
||||
setIsPaused(true);
|
||||
setTimeout(() => setIsPaused(false), 1000);
|
||||
};
|
||||
|
||||
const [activeIndex1, setActiveIndex1] = useState(0);
|
||||
const [isPaused1, setIsPaused1] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPaused1) {
|
||||
const interval = setInterval(() => {
|
||||
setActiveIndex1((prevIndex) => (prevIndex + 1) % howItWorks.length);
|
||||
}, 3000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [isPaused1, howItWorks.length]);
|
||||
|
||||
const handleFeatureClick1 = (index) => {
|
||||
setActiveIndex1(index);
|
||||
setIsPaused1(true);
|
||||
setTimeout(() => setIsPaused1(false), 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen overflow-x-hidden bg-white">
|
||||
{/* Hero Section */}
|
||||
<div
|
||||
id="about"
|
||||
className="bg-gradient-to-r from-blue-50 to-white min-h-[90vh] flex items-center relative"
|
||||
>
|
||||
<div className="container mx-auto px-4 md:px-6 lg:px-8 relative z-10">
|
||||
<div className="flex flex-col md:flex-row items-center gap-8 lg:gap-12">
|
||||
{/* Left Side - Text Content */}
|
||||
<div className="w-full md:w-1/2 text-center md:text-left order-1 md:order-1">
|
||||
<div className="flex justify-center md:justify-start items-center mb-6 lg:mb-8">
|
||||
<div className="text-cyan-400 mr-2 md:mr-3">
|
||||
<svg
|
||||
className="w-10 md:w-12 h-10 md:h-12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 2L2 12L12 22L22 12L12 2Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-black">
|
||||
{t("skycrate")}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl md:text-2xl font-bold mb-4 md:mb-6 text-black">
|
||||
{t("hero_subtitle")}
|
||||
</h2>
|
||||
|
||||
<p className="text-gray-800 mb-6 md:mb-10 text-base md:text-lg">
|
||||
{t("hero_desc")}
|
||||
</p>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex flex-col sm:flex-row justify-center md:justify-start space-y-4 sm:space-y-0 sm:space-x-4">
|
||||
<Link
|
||||
to="/signup"
|
||||
className="bg-emerald-500 hover:bg-emerald-600 text-white font-medium rounded-full px-6 py-4 md:px-8 md:py-6 transform hover:scale-105 transition-all duration-300 shadow-lg hover:shadow-xl"
|
||||
>
|
||||
{t("get_started")}
|
||||
</Link>
|
||||
<Link
|
||||
to="/login"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-full px-6 py-4 md:px-8 md:py-6 transform hover:scale-105 transition-all duration-300 shadow-lg hover:shadow-xl"
|
||||
>
|
||||
{t("login")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{/* Right Side - Image */}
|
||||
<div className="w-full md:w-1/2 flex justify-center order-2 md:order-1">
|
||||
<div className="relative p-4 bg-gradient-to-r from-blue-50 to-emerald-50 rounded-2xl max-w-xs sm:max-w-lg md:max-w-md lg:max-w-lvh">
|
||||
<img
|
||||
src="/Dashboard.png"
|
||||
alt="Skycrate Dashboard Interface"
|
||||
className="w-full rounded-xl shadow-2xl transition-shadow duration-300"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-500/5 to-emerald-500/5 rounded-2xl pointer-events-none"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features Section */}
|
||||
<div
|
||||
id="features"
|
||||
className="w-full max-w-5xl mx-auto p-6 sm:p-8 bg-gray-100 rounded-lg shadow-lg"
|
||||
>
|
||||
<h2 className="text-3xl font-bold text-center mb-8">{t("key_features")}</h2>
|
||||
<div className="flex flex-col-reverse md:flex-row items-center gap-8 lg:gap-12">
|
||||
{/* Left Side - Image */}
|
||||
<div className="w-full md:w-1/2 flex justify-center">
|
||||
<img
|
||||
src="/He.png"
|
||||
alt="Feature Illustration"
|
||||
className="w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg object-contain rounded-lg shadow-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Side - Feature List */}
|
||||
<div className="w-full md:w-1/2">
|
||||
<div className="space-y-6">
|
||||
{features.map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-5 border-2 rounded-lg cursor-pointer transition-all duration-500 ${
|
||||
index === activeIndex
|
||||
? "border-blue-500 bg-white shadow-lg scale-105"
|
||||
: "border-gray-300"
|
||||
}`}
|
||||
onClick={() => handleFeatureClick(index)}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
{feature.icon}
|
||||
<h3 className="text-lg font-semibold">{feature.title}</h3>
|
||||
</div>
|
||||
{index === activeIndex && (
|
||||
<p className="text-gray-600 mt-3 transition-opacity duration-500 opacity-100">
|
||||
{feature.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* How It Works Section */}
|
||||
<div
|
||||
id="howItWorks"
|
||||
className="w-full max-w-5xl mx-auto p-6 sm:p-8 bg-gray-100 rounded-lg shadow-lg"
|
||||
>
|
||||
<h2 className="text-3xl font-bold text-center mb-8">{t("how_it_works")}</h2>
|
||||
<div className="flex flex-col md:flex-row items-center gap-8 lg:gap-12">
|
||||
{/* Left Side - Feature List */}
|
||||
<div className="w-full md:w-1/2">
|
||||
<div className="space-y-6">
|
||||
{howItWorks.map((howItWork, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-5 border-2 rounded-lg cursor-pointer transition-all duration-500 ${
|
||||
index === activeIndex1
|
||||
? "border-blue-500 bg-white shadow-lg scale-105"
|
||||
: "border-gray-300"
|
||||
}`}
|
||||
onClick={() => handleFeatureClick1(index)}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
{howItWork.icon}
|
||||
<h3 className="text-lg font-semibold">{howItWork.title}</h3>
|
||||
</div>
|
||||
{index === activeIndex1 && (
|
||||
<p className="text-gray-600 mt-3 transition-opacity duration-500 opacity-100">
|
||||
{howItWork.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side - Image */}
|
||||
<div className="w-full md:w-1/2 flex justify-center">
|
||||
<img
|
||||
src="/She.png"
|
||||
alt="Feature Illustration"
|
||||
className="w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg object-contain rounded-lg shadow-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DrivethruLandingPage;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next"; // for multilinguality
|
||||
|
||||
const NotFoundPage = () => {
|
||||
const { t } = useTranslation(); // for multilinguality
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-screen bg-gray-100 p-4">
|
||||
<img
|
||||
src="/404.png"
|
||||
style={{ width: "30%", height: "auto" }}
|
||||
alt="404 Not Found"
|
||||
/>
|
||||
<h2 className="text-2xl font-bold mb-4 mt-4">{t("not_found_title")}</h2>
|
||||
<p className="text-center text-gray-700 mb-6">
|
||||
{t("not_found_description")}
|
||||
</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="px-6 py-2 bg-[#1877F2] text-white rounded hover:bg-blue-600 transition duration-200"
|
||||
>
|
||||
{t("go_home")}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFoundPage;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
const initialState = {
|
||||
isUploading: false,
|
||||
};
|
||||
|
||||
const uploadStatusSlice = createSlice({
|
||||
name: "uploadStatus",
|
||||
initialState,
|
||||
reducers: {
|
||||
setIsUploading: (state) => {
|
||||
state.isUploading = !state.isUploading;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setIsUploading } = uploadStatusSlice.actions;
|
||||
export default uploadStatusSlice.reducer;
|
||||
@@ -0,0 +1,19 @@
|
||||
// src/redux/pathSlice.js
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
const initialState = {
|
||||
currentPath: "/",
|
||||
};
|
||||
|
||||
const pathSlice = createSlice({
|
||||
name: "path",
|
||||
initialState,
|
||||
reducers: {
|
||||
setCurrentPath: (state, action) => {
|
||||
state.currentPath = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setCurrentPath } = pathSlice.actions;
|
||||
export default pathSlice.reducer;
|
||||
@@ -0,0 +1,11 @@
|
||||
// src/redux/store.js
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import pathReducer from "./pathSlice";
|
||||
import setIsUploadingReducer from "./UploadStatusSlice";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
path: pathReducer,
|
||||
upload: setIsUploadingReducer,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss(),
|
||||
],
|
||||
server: {
|
||||
host: 'localhost',
|
||||
port: 5173,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,54 @@
|
||||
# CC-MINI (2025)
|
||||
# Skycrate
|
||||
|
||||
`deployment` branch is intended only for pushing deployment files.
|
||||
> [!NOTE]
|
||||
> This project is now multilingual. To contribute new languages, please read the [translation guide](./TRANSLATION.md).
|
||||
|
||||
---
|
||||
|
||||
## Versions
|
||||
- Hadoop: 3.4.1
|
||||
- Java: 17
|
||||
- Node: 22.14.0
|
||||
- NPM: 10.9.2
|
||||
|
||||
## How to run?
|
||||
|
||||
> [!IMPORTANT]
|
||||
> You must have [Docker](https://www.docker.com/products/docker-desktop/) and [Git](https://git-scm.com/) installed on your system.
|
||||
|
||||
1. Clone this repository:
|
||||
|
||||
```shell
|
||||
git clone https://git.kska.io/notkshitij/Skycrate.git
|
||||
```
|
||||
|
||||
2. Change into the directory:
|
||||
|
||||
```shell
|
||||
cd ./Skycrate
|
||||
```
|
||||
|
||||
3. Create a `.env` file inside the directory containing the following variables:
|
||||
|
||||
```env
|
||||
MYSQL_PASSWORD=<set-a-strong-password>
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Please choose a strong password, since it will be used for your MySQL database.
|
||||
|
||||
4. Execute the Docker Compose file:
|
||||
|
||||
```shell
|
||||
docker-compose -f docker-compose.yaml up -d
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> Use `-d` flag to run in detached mode.
|
||||
|
||||
5. Visit `localhost:8080` to enjoy using Skycrate!
|
||||
|
||||
> [!NOTE]
|
||||
> To stop and remove all the containers, run `docker compose down`
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# TRANSLATION
|
||||
|
||||
This is a comprehensive guide for translation for those who wish to contribute in any language.
|
||||
|
||||
---
|
||||
|
||||
## 1. Add Your Language JSON File
|
||||
|
||||
- Go to the `Frontend/src/locales/` directory.
|
||||
- Copy an existing language file (e.g., `en.json`) and rename it to your language code (e.g., `es.json` for Spanish, `de.json` for German).
|
||||
- Translate all the key-value pairs in your new file.
|
||||
|
||||
**Example:**
|
||||
|
||||
```shell
|
||||
cp Frontend/src/locales/en.json Frontend/src/locales/es.json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"skycrate": "Skycrate",
|
||||
"hero_subtitle": "Store, Access & Share Your Files — Anytime, Anywhere!",
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Register the Language in `Frontend/src/i18n.js`
|
||||
|
||||
- Open `Frontend/src/i18n.js`.
|
||||
- Import your new JSON file:
|
||||
|
||||
```js
|
||||
import en from './locales/en.json';
|
||||
import fr from './locales/fr.json';
|
||||
// import more languages as needed
|
||||
import es from './locales/es.json'; // <-- Add this line
|
||||
|
||||
const resources = {
|
||||
en: { translation: en },
|
||||
fr: { translation: fr },
|
||||
// add other languages here
|
||||
es: { translation: es }, // <-- Add this line
|
||||
};
|
||||
```
|
||||
|
||||
## 3. Update the Language Switcher
|
||||
|
||||
- Open `Frontend/src/components/LanguageSwitcher.jsx`.
|
||||
- Add your language to the `languages` array:
|
||||
|
||||
|
||||
```js
|
||||
const languages = [
|
||||
{ code: 'en', label: 'English' },
|
||||
{ code: 'fr', label: 'Français' },
|
||||
// Add more languages as needed
|
||||
{ code: 'es', label: 'Spanish' }, // <-- Add this line
|
||||
];
|
||||
```
|
||||
|
||||
## 4. Test Your Translation
|
||||
|
||||
- Start the app.
|
||||
- Use the language switcher to select your new language.
|
||||
- Check all pages for missing or untranslated keys.
|
||||
- If you see a key instead of a translation, add it to your JSON file.
|
||||
|
||||
## 5. Submit Your Contribution
|
||||
|
||||
- Double-check your translations for accuracy and completeness.
|
||||
- Commit your changes to:
|
||||
- `Frontend/src/locales/<your_language>.json`
|
||||
- `Frontend/src/i18n.js`
|
||||
- `Frontend/src/components/LanguageSwitcher.jsx`
|
||||
- Open a pull request with a description of your contribution.
|
||||
|
||||
---
|
||||
|
||||
## Thank you for making Skycrate accessible to more people!
|
||||
@@ -0,0 +1,151 @@
|
||||
services:
|
||||
namenode:
|
||||
image: kshitijka/hadoop-namenode:3.4.1
|
||||
container_name: skycrate-hadoop-namenode
|
||||
restart: on-failure:5
|
||||
ports:
|
||||
- "9870:9870" # Web UI
|
||||
#- "9000:9000" # Hadoop; No need to expose since backend will access internally
|
||||
user: "hdoop:hdoop"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
networks:
|
||||
- skycrate-internal
|
||||
volumes:
|
||||
- skycrate-hadoop_namenode:/hadoop/dfs/name
|
||||
environment:
|
||||
- CLUSTER_NAME=skycreate
|
||||
env_file:
|
||||
- ./hadoop.env
|
||||
|
||||
datanode:
|
||||
image: kshitijka/hadoop-datanode:3.4.1
|
||||
container_name: skycrate-hadoop-datanode-1
|
||||
restart: on-failure:5
|
||||
user: "hdoop:hdoop"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
networks:
|
||||
- skycrate-internal
|
||||
volumes:
|
||||
- skycrate-hadoop_datanode:/hadoop/dfs/data
|
||||
environment:
|
||||
SERVICE_PRECONDITION: "namenode:9870"
|
||||
env_file:
|
||||
- ./hadoop.env
|
||||
# healthcheck:
|
||||
# disable: true
|
||||
|
||||
resourcemanager:
|
||||
image: kshitijka/hadoop-resourcemanager:3.4.1
|
||||
container_name: skycrate-hadoop-resourcemanager
|
||||
restart: on-failure:10
|
||||
user: "hdoop:hdoop"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
networks:
|
||||
- skycrate-internal
|
||||
environment:
|
||||
SERVICE_PRECONDITION: "namenode:9000 namenode:9870 datanode:9864"
|
||||
env_file:
|
||||
- ./hadoop.env
|
||||
# healthcheck:
|
||||
# disable: true
|
||||
|
||||
nodemanager:
|
||||
image: kshitijka/hadoop-nodemanager:3.4.1
|
||||
container_name: skycrate-hadoop-nodemanager
|
||||
restart: on-failure:5
|
||||
user: "hdoop:hdoop"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
networks:
|
||||
- skycrate-internal
|
||||
environment:
|
||||
SERVICE_PRECONDITION: "namenode:9000 namenode:9870 datanode:9864 resourcemanager:8088"
|
||||
env_file:
|
||||
- ./hadoop.env
|
||||
# healthcheck:
|
||||
# disable: true
|
||||
|
||||
historyserver:
|
||||
image: kshitijka/hadoop-historyserver:3.4.1
|
||||
container_name: skycrate-hadoop-historyserver
|
||||
restart: on-failure:5
|
||||
user: "hdoop:hdoop"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
networks:
|
||||
- skycrate-internal
|
||||
environment:
|
||||
SERVICE_PRECONDITION: "namenode:9000 namenode:9870 datanode:9864 resourcemanager:8088"
|
||||
volumes:
|
||||
- skycrate-hadoop_historyserver:/hadoop/yarn/timeline
|
||||
env_file:
|
||||
- ./hadoop.env
|
||||
# healthcheck:
|
||||
# disable: true
|
||||
|
||||
db:
|
||||
image: mysql:8
|
||||
container_name: skycrate-db
|
||||
restart: on-failure:5
|
||||
user: "1000:1000"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
networks:
|
||||
- skycrate-internal
|
||||
environment:
|
||||
- MYSQL_DATABASE=skycrate
|
||||
- MYSQL_USER=skycrateDB
|
||||
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
|
||||
- MYSQL_RANDOM_ROOT_PASSWORD=yes
|
||||
volumes:
|
||||
- skycrate-db:/var/lib/mysql
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
frontend:
|
||||
image: kshitijka/skycrate-frontend:1.0
|
||||
container_name: skycrate-frontend
|
||||
restart: on-failure:5
|
||||
user: "skycrateFront:skycrateFront"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
networks:
|
||||
- skycrate-internal
|
||||
ports:
|
||||
- "80:8080"
|
||||
volumes:
|
||||
- skycrate-frontend:/app
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
backend:
|
||||
image: kshitijka/skycrate-backend:1.0
|
||||
container_name: skycrate-backend
|
||||
restart: on-failure:5
|
||||
user: "skycrateBack:skycrateBack"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
networks:
|
||||
- skycrate-internal
|
||||
ports:
|
||||
- "8081:8081" # If you change, update in Frontend/.env file too and rebuild the image
|
||||
environment:
|
||||
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
|
||||
volumes:
|
||||
- skycrate-backend:/app
|
||||
|
||||
volumes:
|
||||
skycrate-hadoop_namenode:
|
||||
skycrate-hadoop_datanode:
|
||||
skycrate-hadoop_historyserver:
|
||||
skycrate-db:
|
||||
skycrate-frontend:
|
||||
skycrate-backend:
|
||||
|
||||
networks:
|
||||
skycrate-internal:
|
||||
external: false
|
||||
driver: bridge
|
||||
@@ -1,15 +0,0 @@
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
hF4DgDtqgMBzTpASAQdAN9Hlcs7Jwec23tYTzz4fnF8+i8sq9hHUR/gwxt4bEzww
|
||||
4/pSfujLd/Hjf95kf1xAJzKw2K0KxKEG6JpWoSNzblKROKnTDshiusYC3ZS4Lzva
|
||||
1MDfAQkCEFd3bEysTE5nhDwCnguTGVMQLVeRcxCxXARG85BSZMCAPBbUzDk0pGU2
|
||||
nl6aiDJEi6Dmh6Du4eIFHhqt+P6L6KKbfLwkclrJX8HI/6n5as4xgDc1rRyqs+11
|
||||
HDzz+ZM+2T5cLExA5GqDD5o0hFAMURtKmaI/C6W5QWjuBs6cQCrFWE3XcfT5daYg
|
||||
eG+y5d8ZvQOytsAkWX6GgSqV0YBRuqbVJh+JpBT9j62LndMR7We297gw+K7nM+/Y
|
||||
r1ZBJFduIT/D4fZbhpf63bRTZO7BO9RGgHPhpubzkb14aznbJlGJjWSzbIetr2uT
|
||||
NKb8JqUY+Kd89299rewXozdwYFfjkT8RYMxhdwlREQD4F9pweaLAaOYFDv7JCqbi
|
||||
hhog/JLw7tv6vcaTOKEDsfnPZemOz65PT6hNevxOUsuHQ89hB7X8nJihXMZqPXva
|
||||
X5dZTwVSzRBk6isrDUU3HqSP+HHYAygpm1uTKJFoavu4WCz9APFjiRoxYQQuA5Yz
|
||||
HUqga7xBYTs3hOHwiSY56gzCm6CA3Z7JbQqcTOfkC8To9w==
|
||||
=rSxA
|
||||
-----END PGP MESSAGE-----
|
||||
@@ -0,0 +1,43 @@
|
||||
CORE_CONF_fs_defaultFS=hdfs://namenode:9000
|
||||
CORE_CONF_hadoop_http_staticuser_user=root
|
||||
CORE_CONF_hadoop_proxyuser_hue_hosts=*
|
||||
CORE_CONF_hadoop_proxyuser_hue_groups=*
|
||||
CORE_CONF_io_compression_codecs=org.apache.hadoop.io.compress.SnappyCodec
|
||||
|
||||
HDFS_CONF_dfs_webhdfs_enabled=true
|
||||
HDFS_CONF_dfs_permissions_enabled=false
|
||||
HDFS_CONF_dfs_namenode_datanode_registration_ip___hostname___check=false
|
||||
|
||||
YARN_CONF_yarn_log___aggregation___enable=true
|
||||
YARN_CONF_yarn_log_server_url=http://historyserver:8188/applicationhistory/logs/
|
||||
YARN_CONF_yarn_resourcemanager_recovery_enabled=true
|
||||
YARN_CONF_yarn_resourcemanager_store_class=org.apache.hadoop.yarn.server.resourcemanager.recovery.FileSystemRMStateStore
|
||||
YARN_CONF_yarn_resourcemanager_scheduler_class=org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CapacityScheduler
|
||||
YARN_CONF_yarn_scheduler_capacity_root_default_maximum___allocation___mb=8192
|
||||
YARN_CONF_yarn_scheduler_capacity_root_default_maximum___allocation___vcores=4
|
||||
YARN_CONF_yarn_resourcemanager_fs_state___store_uri=/rmstate
|
||||
YARN_CONF_yarn_resourcemanager_system___metrics___publisher_enabled=true
|
||||
YARN_CONF_yarn_resourcemanager_hostname=resourcemanager
|
||||
YARN_CONF_yarn_resourcemanager_address=resourcemanager:8032
|
||||
YARN_CONF_yarn_resourcemanager_scheduler_address=resourcemanager:8030
|
||||
YARN_CONF_yarn_resourcemanager_resource__tracker_address=resourcemanager:8031
|
||||
YARN_CONF_yarn_timeline___service_enabled=true
|
||||
YARN_CONF_yarn_timeline___service_generic___application___history_enabled=true
|
||||
YARN_CONF_yarn_timeline___service_hostname=historyserver
|
||||
YARN_CONF_mapreduce_map_output_compress=true
|
||||
YARN_CONF_mapred_map_output_compress_codec=org.apache.hadoop.io.compress.SnappyCodec
|
||||
YARN_CONF_yarn_nodemanager_resource_memory___mb=16384
|
||||
YARN_CONF_yarn_nodemanager_resource_cpu___vcores=8
|
||||
YARN_CONF_yarn_nodemanager_disk___health___checker_max___disk___utilization___per___disk___percentage=98.5
|
||||
YARN_CONF_yarn_nodemanager_remote___app___log___dir=/app-logs
|
||||
YARN_CONF_yarn_nodemanager_aux___services=mapreduce_shuffle
|
||||
|
||||
MAPRED_CONF_mapreduce_framework_name=yarn
|
||||
MAPRED_CONF_mapred_child_java_opts=-Xmx4096m
|
||||
MAPRED_CONF_mapreduce_map_memory_mb=4096
|
||||
MAPRED_CONF_mapreduce_reduce_memory_mb=8192
|
||||
MAPRED_CONF_mapreduce_map_java_opts=-Xmx3072m
|
||||
MAPRED_CONF_mapreduce_reduce_java_opts=-Xmx6144m
|
||||
MAPRED_CONF_yarn_app_mapreduce_am_env=HADOOP_MAPRED_HOME=/opt/hadoop-3.4.1/
|
||||
MAPRED_CONF_mapreduce_map_env=HADOOP_MAPRED_HOME=/opt/hadoop-3.4.1/
|
||||
MAPRED_CONF_mapreduce_reduce_env=HADOOP_MAPRED_HOME=/opt/hadoop-3.4.1/
|
||||