35 Commits

Author SHA1 Message Date
Atharva Ombase 7e86beab1f Add initial project configuration files and PasswordForDownload component for secure file access 2025-08-03 19:51:36 +05:30
Atharva Ombase a12a53a44a Enhance SignUp component with username field, validation, and loading state 2025-08-03 19:27:15 +05:30
Atharva Ombase 0cd4738d09 Enhance Login component with improved validation, loading state, and UI updates 2025-08-03 19:27:07 +05:30
Atharva Ombase dff2de5ddd Add PasswordForDownload component for secure file access with password protection 2025-08-03 19:27:00 +05:30
Atharva Ombase 7597f52b47 Update file upload API endpoint and add authorization header for secure uploads 2025-08-03 19:26:53 +05:30
Atharva Ombase 5283a2b9f1 Refactor FileList component to open a download modal instead of direct downloads; added PasswordForDownload component for secure file access. 2025-08-03 19:26:45 +05:30
Kshitij 8f32c77f7e Updated backend submodule. 2025-07-23 14:55:57 +05:30
Kshitij 2db526d949 Updated Backend submodule. 2025-07-04 01:47:12 +05:30
Kshitij 17046735da Removed unused packages, updated exising packages and added missing depedencies for frontend. 2025-06-27 00:42:44 +05:30
Kshitij 5cd80fe27e Moved translation guide from Frontend/src to root dir. Also added link in README for the guide. 2025-06-27 00:35:19 +05:30
Kshitij d2e9f80c30 Merged frontend-multilingual branch. This project is now multilingual!
Currently supports the following languages:
- English
- Hindi
- Marathi
- French
2025-06-27 00:28:34 +05:30
Kshitij 43d04c7f93 Added Marathi (मराठी) translation! 2025-06-27 00:27:35 +05:30
Kshitij 1cbd74b6a5 Added Hindi (हिंदी) translation! 2025-06-27 00:25:35 +05:30
Kshitij d88f1b6e0b Added guide for contributing in other languages! 2025-06-27 00:16:06 +05:30
Kshitij 73a1c521d5 Performed translation for some components in ./src
- Footer.jsx
- Sidebar.jsx

Also, added key:value pairs (English + French for now) for all the text in src/locales. Need to perform translation for FileList.jsx & FileUploadModal.jsx. Will do once backend is accessible again.
2025-06-27 00:00:33 +05:30
Kshitij 9050bbc5cf Added .vite/ to .gitignore 2025-06-26 23:32:23 +05:30
Kshitij 2cce8d89ca Performed translation for Authentication in src/pages/
- Login.jsx
- SignUp.jsx

Also, added key:value pairs (English + French for now) for all the text in src/locales.
2025-06-26 23:29:24 +05:30
Kshitij 41435aa4fc Performed translation for UserPages in src/pages/
- Dashboard.jsx
- DrivethruLandingPage.jsx
- NotFoundPage.jsx

Also, added key:value pairs (English + French for now) for all the text in src/locales.
2025-06-26 23:20:50 +05:30
Kshitij 7dc8a49a8d Added the foundation for making the frontend multilingual. Now only need to import useTranslation and use t('key') for all user-facing text for all pages (+add key:value pairs in src/locales for each language ofc)
- package.json -> Imported packages i18next & react-i18next for multilingual functionality.
- src/App.jsx -> Imported LanguageSwitcher from ./components/LanguageSwitcher and added <LanguageSwitcher /> component at the beginning of layout so it's always visible.
- src/components/LanguageSwitcher.jsx -> LanguageSwitcher component, consists of a dropdown menu that always appears at top right corner for choosing language.
- src/i18n.js -> Initialize and configure i18next for app-wide multilingual support.
- src/locales/en.json + src/locales/fr.json -> Empty json files that will soon contain translation key:value pairs for each page.
- src/main.jsx -> Imported src/i18n.js for multilingual functionality.
2025-06-26 23:06:25 +05:30
Kshitij 816d115fbc Updated frontend port and added note to stop and remove all containers. 2025-04-21 10:41:45 +05:30
Kshitij 715f3a9d96 Updated backend submodule. 2025-04-21 10:34:27 +05:30
Kshitij 3febc68b4e fixed typo 2025-04-20 22:43:32 +05:30
Kshitij f06967708d Added hadoop env file. 2025-04-20 22:16:55 +05:30
Kshitij f79435d64f Added instructions to deploy the app. 2025-04-20 21:11:12 +05:30
Kshitij 04ac930900 Added docker-compose file. 2025-04-20 21:00:21 +05:30
Kshitij d8193f8174 Updated backend submodule. 2025-04-20 20:54:24 +05:30
Kshitij d08b0d6f90 Ref updated backend submodule. 2025-04-20 04:27:23 +05:30
Kshitij 3b027e4a39 Changed backend:8080 to localhost:8081, fixed cors issue! 2025-04-20 04:26:05 +05:30
Kshitij 49f57b5c10 Ref updated backend submodule 2025-04-20 03:57:50 +05:30
Kshitij ac75a64ec8 Added Backend as submodule to https://git.kska.io/notkshitij/SkycrateBackend.git 2025-04-20 03:41:39 +05:30
Kshitij 0e195ac079 Deleted old Backend/Skycrate submodule, planning to move it to Backend dir. directly. 2025-04-20 03:40:57 +05:30
Kshitij 6358e7e72d Updated backend url in .env and removed or operator in API URL var in Frontend/src/components/FileList.jsx 2025-04-20 02:34:54 +05:30
Kshitij 97be5d1b93 Added Dockerfile to build frontend Docker image. 2025-04-20 02:32:09 +05:30
Kshitij c42a9dacf0 Added skycrate as module in backend directory. 2025-04-19 23:34:49 +05:30
Kshitij 4b929bb272 Updated info file. 2025-04-19 23:31:29 +05:30
32 changed files with 1400 additions and 376 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "Backend"]
path = Backend
url = https://git.kska.io/notkshitij/SkycrateBackend.git
+9
View File
@@ -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>
+8
View File
@@ -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>
+31
View File
@@ -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>
+3 -2
View File
@@ -5,10 +5,11 @@
## Work distribution
- Design: Kapil
- Frontend: Shivani, Shriniwas, Ombase, Tejas, Sonali, Dinesh
- Backend: Vedang, Lalit
- Frontend: Ombase, Shriniwas, Dinesh, Lalit, Shivani, Pracheta, Vaibhavi
- Backend: Vedang, Sonali, Lalit
- DBMS: Lalit
- HDFS: Sonali, Prajakta, Poonam
- Deployment: Kshitij, Sahil
---
Submodule
+1
Submodule Backend added at 2622667de4
+1 -1
View File
@@ -1 +1 @@
VITE_API_URL=http://192.168.29.61:8081
VITE_API_URL=http://localhost:8081
+4
View File
@@ -23,3 +23,7 @@ package-lock.json
*.njsproj
*.sln
*.sw?
#########
.vite/
+28
View File
@@ -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"]
+15 -17
View File
@@ -10,30 +10,28 @@
"preview": "vite preview"
},
"dependencies": {
"@reduxjs/toolkit": "^2.6.0",
"@tailwindcss/vite": "^4.0.9",
"axios": "^1.8.4",
"@reduxjs/toolkit": "^2.8.2",
"@tailwindcss/vite": "^4.1.11",
"i18next": "^25.2.1",
"lucide-react": "^0.476.0",
"react": "^19.0.0",
"react-dom": "^19.0.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.2.0"
"react-router-dom": "^7.6.2"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.21.0",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.19",
"@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",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.9",
"vite": "^6.2.0"
"vite": "^6.3.5"
}
}
+2
View File
@@ -1,5 +1,6 @@
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";
@@ -9,6 +10,7 @@ import NotFoundPage from "./pages/UserPages/NotFoundPage";
function App() {
return (
<Router>
<LanguageSwitcher />
<Routes>
<Route path="/" element={<DrivethruLandingPage />} />
<Route path="/login" element={<Login />} />
+94 -108
View File
@@ -2,6 +2,7 @@ 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,
@@ -18,18 +19,18 @@ import {
ArrowLeft,
} from "lucide-react";
const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8080";
const API_URL = import.meta.env.VITE_API_URL;
const FileTable = ({ initialPath }) => {
// Read username dynamically to avoid stale null on first load
const FileList = ({ initialPath }) => {
const username = localStorage.getItem("username") || "";
const userRoot = `/${username}`;
// Initialize currentPath only once with the correct userRoot
const [currentPath, setCurrentPathState] = useState(
() => initialPath || userRoot
);
const [files, setFiles] = useState([]);
const [showDownloadModal, setShowDownloadModal] = useState(false);
const [downloadFilename, setDownloadFilename] = useState("");
const dispatch = useDispatch();
const isUploading = useSelector((state) => state.upload.isUploading);
@@ -41,7 +42,6 @@ const FileTable = ({ initialPath }) => {
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":
@@ -111,7 +111,6 @@ const FileTable = ({ initialPath }) => {
: `${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();
@@ -136,116 +135,103 @@ const FileTable = ({ initialPath }) => {
setCurrentPathState(parts.length === 0 ? userRoot : `/${parts.join("/")}`);
};
const handleFileDownload = async (hdfsPath, name, event) => {
event.stopPropagation();
try {
const authToken = localStorage.getItem("token");
const response = await fetch(
`${API_URL}/api/hdfs/downloadFile?hdfsEncPath=${encodeURIComponent(
hdfsPath
)}&localPath=${name}&username=${username}`,
{
method: "POST",
headers: { Authorization: `Bearer ${authToken}` },
}
);
if (!response.ok) throw new Error(await response.text());
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = name;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
fetchFiles();
} catch (error) {
console.error("Download failed:", error);
alert("Something went wrong while downloading the file.");
}
// 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 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>
<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) => handleFileDownload(hdfsPath, 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)}
/>
)}
</>
);
};
FileTable.propTypes = {
FileList.propTypes = {
initialPath: PropTypes.string,
};
export default FileTable;
export default FileList;
+5 -1
View File
@@ -43,10 +43,14 @@ const FileUploadModal = ({ show, onClose, onUploadSuccess }) => {
try {
setUploading(true);
setUploadMessage("⏳ Uploading file...");
const response = await fetch(`${API_URL}/api/hdfs/uploadFile`, {
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}`);
+21 -20
View File
@@ -8,15 +8,17 @@ import {
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("You have successfully subscribed!");
alert(t("subscribe_success"));
setEmail("");
}
};
@@ -42,10 +44,10 @@ const Footer = () => {
/>
</svg>
</div>
<h3 className="text-2xl font-bold text-white">Skycrate</h3>
<h3 className="text-2xl font-bold text-white">{t("footer_brand")}</h3>
</div>
<p className="text-white/90">
Your secure cloud storage solution for all your digital needs.
{t("footer_tagline")}
</p>
<div className="flex space-x-4">
<a
@@ -81,14 +83,14 @@ const Footer = () => {
{/* Quick Links */}
<div>
<h4 className="font-semibold text-white mb-4">Quick Links</h4>
<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"
>
About Us
{t("footer_about_us")}
</a>
</li>
<li>
@@ -96,7 +98,7 @@ const Footer = () => {
href="#features"
className="text-white/90 hover:text-white transition-all duration-200 hover:translate-x-1 inline-block"
>
Features
{t("footer_features")}
</a>
</li>
<li>
@@ -104,7 +106,7 @@ const Footer = () => {
href="#howItWorks"
className="text-white/90 hover:text-white transition-all duration-200 hover:translate-x-1 inline-block"
>
How It Works
{t("footer_how_it_works")}
</a>
</li>
</ul>
@@ -112,43 +114,42 @@ const Footer = () => {
{/* Contact Info */}
<div>
<h4 className="font-semibold text-white mb-4">Contact</h4>
<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" />
support@drivethru.com
{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" />
+91 3628206234
{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" />
123 Cloud Street, Digital City
{t("footer_address")}
</li>
</ul>
</div>
{/* Newsletter */}
<div>
<h4 className="font-semibold text-white mb-4">Stay Updated</h4>
<h4 className="font-semibold text-white mb-4">{t("footer_newsletter_title")}</h4>
<p className="text-white/90 mb-4">
Get exclusive tips, updates on new features, and special offers
directly in your inbox.
{t("footer_newsletter_desc")}
</p>
<div className="space-y-4">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
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"
>
Subscribe to Newsletter
{t("footer_newsletter_button")}
</button>
</div>
</div>
@@ -158,25 +159,25 @@ const Footer = () => {
{/* Bottom Section */}
<div className="flex flex-col md:flex-row justify-between items-center text-white/90 text-sm">
<p>© {new Date().getFullYear()} Skycrate. All rights reserved.</p>
<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"
>
Privacy Policy
{t("footer_privacy_policy")}
</a>
<a
href="#"
className="hover:text-white transition-all duration-200 hover:translate-x-1 inline-block"
>
Terms of Service
{t("footer_terms_of_service")}
</a>
<a
href="#"
className="hover:text-white transition-all duration-200 hover:translate-x-1 inline-block"
>
Cookie Policy
{t("footer_cookie_policy")}
</a>
</div>
</div>
@@ -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;
+10 -8
View File
@@ -1,15 +1,17 @@
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("Logging out...");
const loadingToast = toast.loading(t("sidebar_logging_out"));
// Simulate a delay (for example, network request)
setTimeout(() => {
@@ -23,7 +25,7 @@ const Sidebar = () => {
// Show success toast after logout
toast.update(loadingToast, {
render: "Logged out successfully!",
render: t("sidebar_logged_out"),
type: "success",
isLoading: false,
autoClose: 2000,
@@ -55,7 +57,7 @@ const 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">Open sidebar</span>
<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"
@@ -67,7 +69,7 @@ const Sidebar = () => {
<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">
Skycrate
{t("sidebar_brand")}
</span>
</Link>
</div>
@@ -84,11 +86,11 @@ const Sidebar = () => {
onClick={() => setUserMenuOpen((o) => !o)}
className="flex text-lg bg-gray-800 rounded-full focus:ring-4 focus:ring-gray-300"
>
<span className="sr-only">Open user menu</span>
<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="User Photo"
alt={t("sidebar_user_photo")}
/>
</button>
@@ -109,7 +111,7 @@ const Sidebar = () => {
className="w-full text-left px-4 py-2 text-lg text-white hover:bg-[#37A0EA]"
role="menuitem"
>
Log out
{t("sidebar_logout")}
</button>
</li>
</ul>
@@ -139,7 +141,7 @@ const Sidebar = () => {
>
<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">Starred</span>
<span className="ms-3">{t("sidebar_starred")}</span>
</Link>
</li>
{/* ...additional sidebar items... */}
+30
View File
@@ -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;
+94
View File
@@ -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": "Dont 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"
}
+92
View File
@@ -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"
}
+92
View File
@@ -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": "चिह्नित"
}
+92
View File
@@ -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": "आवडते"
}
+2
View File
@@ -1,3 +1,5 @@
import './i18n'; // for multilingual functionality
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
+130 -97
View File
@@ -1,151 +1,184 @@
import { useState, useEffect } from "react";
import { FiEye, FiEyeOff } from "react-icons/fi";
import { FiEye, FiEyeOff, FiLoader } from "react-icons/fi";
import { Link, useNavigate } from "react-router-dom";
import toast from "react-hot-toast"; // Import React Hot Toast
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 [showPassword, setShowPassword] = useState(false);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
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(() => {
// Check if token is present in localStorage and redirect to Dashboard
if (localStorage.getItem("token")) {
navigate("/dashboard"); // Redirect to Dashboard
navigate("/dashboard");
}
}, [navigate]);
const togglePassword = () => {
setShowPassword(!showPassword);
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();
setLoading(true);
const validation = validate();
if (Object.keys(validation).length) {
setErrors(validation);
return;
}
// Show loading toast
const toastId = toast.loading("Logging in...");
setLoading(true);
const toastId = toast.loading(t("logging_in_toast"));
try {
const response = await fetch(`${API_URL}/api/login`, {
const response = await fetch(`${API_URL}/api/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
password,
}),
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const data = await response.json();
// Dismiss the loading toast after the response
toast.dismiss(toastId);
if (response.ok) {
// On success, store the token in localStorage
localStorage.setItem("token", data.token);
localStorage.setItem("expiresIn", data.expiresIn);
// fetch username asynchronously
fetch(`${API_URL}/api/hdfs/getUsernameByEmail?email=${email}`)
.then((response) => response.text())
.then((username) => {
localStorage.setItem("username", username);
})
.catch((error) => {
console.error("Error fetching username:", error);
});
.then((res) => res.text())
.then((username) => localStorage.setItem("username", username))
.catch((err) => console.error("Error fetching username:", err));
// Show success toast
toast.success("Login successful!");
// Redirect to Dashboard
toast.success(t("login_successful"));
navigate("/dashboard");
} else {
// Show error toast if login fails
toast.error(data.message || "Login failed.");
toast.error(data.message || t("login_failed"));
}
} catch (error) {
// Dismiss the loading toast and show error
toast.dismiss(toastId);
toast.error("An error occurred. Please try again.", error);
console.error(error);
toast.error(t("an_error_occurred"));
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center p-4">
<div className="w-full max-w-md bg-white rounded-4xl shadow-lg p-8">
<h1 className="text-2xl font-bold mb-6 text-gray-900 text-center">
Log in
<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}>
<div className="mb-4">
<div className="flex items-center">
<input
type="email"
id="email"
placeholder="Enter your email"
className="w-full border border-gray-300 rounded-l-lg px-4 py-4 focus:outline-none focus:border-blue-500"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
</div>
<div className="mb-1">
<div className="relative">
<input
type={showPassword ? "text" : "password"}
id="password"
placeholder="Enter your password"
className="w-full border border-gray-300 rounded-lg px-4 py-4 focus:outline-none focus:border-blue-500 pr-10"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button
type="button"
onClick={togglePassword}
className="absolute right-2 top-4 text-2xl text-gray-500 hover:text-gray-700"
>
{showPassword ? <FiEyeOff /> : <FiEye />}
</button>
</div>
</div>
<div className="mb-6 ">
<Link
to="#!"
className="text-sm text-blue-600 hover:underline inline-block"
<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"
>
Forgot password?
{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 py-3 bg-gradient-to-r from-[#1877F2] to-[#0E458C] hover:from-[#0E458C] hover:to-[#1877F2] text-white font-semibold rounded-full shadow-md transition duration-300"
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 ? "Logging In..." : "Login"}
{loading ? (
<FiLoader className="animate-spin text-lg" />
) : (
t("login")
)}
</button>
</form>
<div className="text-center mt-6">
<p className="text-gray-700">
Dont have an account?{" "}
<Link
to="/signup"
className="text-emerald-500 hover:underline font-medium"
>
Sign up
</Link>
</p>
</div>
<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>
);
+128 -66
View File
@@ -1,16 +1,19 @@
import { useState } from "react";
import { FiEye, FiEyeOff } from "react-icons/fi";
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: "",
@@ -18,28 +21,38 @@ const SignUp = () => {
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) => {
setFormData((prev) => ({
...prev,
[e.target.name]: e.target.value,
}));
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
setErrors((prev) => ({ ...prev, [name]: undefined }));
};
const handleSubmit = async (e) => {
e.preventDefault();
if (formData.password !== formData.confirmPassword) {
toast.error("Passwords do not match.");
const validation = validate();
if (Object.keys(validation).length) {
setErrors(validation);
return;
}
setLoading(true);
const toastId = toast.loading("Registering...");
const toastId = toast.loading(t("registering"));
try {
// 1️⃣ Sign up the user
const signupRes = await fetch(`${API_URL}/api/signup`, {
const signupRes = await fetch(`${API_URL}/api/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
@@ -47,39 +60,32 @@ const SignUp = () => {
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 || "Signup failed.", { id: toastId });
toast.error(signupData.message || t("signup_failed"), { id: toastId });
return;
}
// 2️⃣ Build username and create HDFS folder
const username =
`${formData.firstname}${formData.lastname}`.toLowerCase();
const folderRes = await fetch(
`${API_URL}/api/hdfs/createFolder?hdfsPath=/${username}`,
`${API_URL}/api/hdfs/createFolder?hdfsPath=/${formData.username}`,
{ method: "POST" }
);
if (!folderRes.ok) {
// you might choose to roll back user creation or just notify
toast.error("Failed to create user folder.", { id: toastId });
toast.error(t("failed_create_folder"), { id: toastId });
} else {
toast.success("Successfully registered and folder created!", {
id: toastId,
});
toast.success(t("signup_success"), { id: toastId });
}
// 3️⃣ Redirect to login after a short delay
setTimeout(() => {
navigate("/login");
}, 1500);
setTimeout(() => navigate("/login"), 1500);
} catch (error) {
console.error(error);
toast.error("An error occurred. Please try again.", { id: toastId });
toast.error(t("an_error_occurred"), { id: toastId });
} finally {
setLoading(false);
}
@@ -89,98 +95,154 @@ const SignUp = () => {
<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">Sign Up</h1>
<form className="space-y-4" onSubmit={handleSubmit}>
<input
type="text"
name="firstname"
placeholder="First Name"
value={formData.firstname}
onChange={handleChange}
className="w-full border border-gray-300 rounded-lg px-4 py-4 focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
<input
type="text"
name="lastname"
placeholder="Last Name"
value={formData.lastname}
onChange={handleChange}
className="w-full border border-gray-300 rounded-lg px-4 py-4 focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
<input
type="email"
name="email"
placeholder="Enter your email"
value={formData.email}
onChange={handleChange}
className="w-full border border-gray-300 rounded-lg px-4 py-4 focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
<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"
placeholder="Enter your password"
value={formData.password}
onChange={handleChange}
className="w-full border border-gray-300 rounded-lg px-4 py-4 focus:outline-none focus:ring-2 focus:ring-blue-500 pr-10"
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-4 text-2xl text-gray-500 hover:text-gray-700"
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"
placeholder="Confirm your password"
value={formData.confirmPassword}
onChange={handleChange}
className="w-full border border-gray-300 rounded-lg px-4 py-4 focus:outline-none focus:ring-2 focus:ring-blue-500 pr-10"
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-4 text-2xl text-gray-500 hover:text-gray-700"
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 ${
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 ? "Signing Up..." : "Sign Up"}
{loading ? (
<FiLoader className="animate-spin text-xl" />
) : (
t("sign_up")
)}
</button>
</form>
{/* Redirect to Login */}
<p className="text-center mt-4 text-gray-700">
Already have an account?{" "}
{t("already_have_account")}{" "}
<Link
to="/login"
className="text-blue-500 hover:underline font-medium"
>
Login
{t("login")}
</Link>
</p>
</div>
+6 -2
View File
@@ -1,16 +1,19 @@
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");
@@ -36,7 +39,7 @@ const Dashboard = () => {
setFiles(data);
} catch (error) {
console.error("Failed to fetch files:", error);
setError("Failed to load files. Please try again later.");
setError(t("failed_to_load_files"));
}
};
@@ -46,6 +49,7 @@ const Dashboard = () => {
} else {
fetchFiles();
}
// eslint-disable-next-line
}, [navigate]);
return (
@@ -54,7 +58,7 @@ const Dashboard = () => {
<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">Dashboard</h1>
<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"
@@ -1,15 +1,17 @@
import Footer from "../../components/Footer";
import React from "react";
import { useState, useEffect } from "react";
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: "Easy Upload & Access",
description: "Drag & drop, instant access.",
title: t("feature_easy_upload_title"),
description: t("feature_easy_upload_desc"),
icon: (
<svg
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
@@ -25,8 +27,8 @@ const DrivethruLandingPage = () => {
),
},
{
title: "Secure & Private",
description: "End-to-end encryption.",
title: t("feature_secure_title"),
description: t("feature_secure_desc"),
icon: (
<svg
className="w-6 h-6"
@@ -44,8 +46,8 @@ const DrivethruLandingPage = () => {
),
},
{
title: "Seamless Sharing",
description: "Share files with one click.",
title: t("feature_sharing_title"),
description: t("feature_sharing_desc"),
icon: (
<svg
className="w-6 h-6"
@@ -63,8 +65,8 @@ const DrivethruLandingPage = () => {
),
},
{
title: "Access Anywhere",
description: "Works on all devices.",
title: t("feature_access_anywhere_title"),
description: t("feature_access_anywhere_desc"),
icon: (
<svg
className="w-6 h-6"
@@ -85,8 +87,8 @@ const DrivethruLandingPage = () => {
const howItWorks = [
{
title: "Create an account",
description: "Sign up in seconds.",
title: t("how_create_account_title"),
description: t("how_create_account_desc"),
icon: (
<svg
className="w-6 h-6"
@@ -104,8 +106,8 @@ const DrivethruLandingPage = () => {
),
},
{
title: "Upload files",
description: "Drag & drop or select from your device.",
title: t("how_upload_files_title"),
description: t("how_upload_files_desc"),
icon: (
<svg
className="w-6 h-6"
@@ -123,10 +125,10 @@ const DrivethruLandingPage = () => {
),
},
{
title: "Manage files",
description: "Rename, move, or delete easily.",
title: t("how_manage_files_title"),
description: t("how_manage_files_desc"),
icon: (
<svg
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
@@ -148,8 +150,8 @@ const DrivethruLandingPage = () => {
),
},
{
title: "Access anytime",
description: "Open files from any device.",
title: t("how_access_anytime_title"),
description: t("how_access_anytime_desc"),
icon: (
<svg
className="w-6 h-6"
@@ -168,6 +170,7 @@ const DrivethruLandingPage = () => {
},
];
// UseEffect and handle....click function to handle set and handle the animation of features..
const [activeIndex, setActiveIndex] = useState(0);
const [isPaused, setIsPaused] = useState(false);
@@ -234,17 +237,16 @@ const DrivethruLandingPage = () => {
</svg>
</div>
<h1 className="text-4xl md:text-5xl font-bold text-black">
Skycrate
{t("skycrate")}
</h1>
</div>
<h2 className="text-xl md:text-2xl font-bold mb-4 md:mb-6 text-black">
Store, Access & Share Your Files Anytime, Anywhere!
{t("hero_subtitle")}
</h2>
<p className="text-gray-800 mb-6 md:mb-10 text-base md:text-lg">
A simple, secure, and fast cloud storage solution for all your
files. Upload, organize, and access with ease.
{t("hero_desc")}
</p>
{/* Buttons */}
@@ -253,13 +255,13 @@ const DrivethruLandingPage = () => {
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"
>
Get Started
{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"
>
Login
{t("login")}
</Link>
</div>
</div>
@@ -283,7 +285,7 @@ const DrivethruLandingPage = () => {
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">Key Features</h2>
<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">
@@ -328,7 +330,7 @@ const DrivethruLandingPage = () => {
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">How It Works</h2>
<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">
+7 -11
View File
@@ -1,29 +1,25 @@
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">
{/* Placeholder SVG - Replace this with your SVG */}
<img
src="/404.png"
style={{ width: "30%", height: "auto" }}
alt="404 Not Found"
></img>
{/* Page number and title */}
<h2 className="text-2xl font-bold mb-4 mt-4">Page Not Found</h2>
{/* Description text */}
/>
<h2 className="text-2xl font-bold mb-4 mt-4">{t("not_found_title")}</h2>
<p className="text-center text-gray-700 mb-6">
Sorry, we couldn&apos;t find the page you were looking for. It may have
been moved or deleted.
{t("not_found_description")}
</p>
{/* Call-to-action button */}
<Link
to="/"
className="px-6 py-2 bg-[#1877F2] text-white rounded hover:bg-blue-600 transition duration-200"
>
Go Home
{t("go_home")}
</Link>
</div>
);
+43 -15
View File
@@ -1,26 +1,54 @@
# CC-MINI (2025)
# Skycrate
> [!NOTE]
> This project is now multilingual. To contribute new languages, please read the [translation guide](./TRANSLATION.md).
---
## Git config
## Versions
- Hadoop: 3.4.1
- Java: 17
- Node: 22.14.0
- NPM: 10.9.2
Create a new directory for this project, and run these following commands for initalizing git:
## 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://github.com/kshitij-ka/cc-mini.git
cd cc-mini
git config --local user.name "Your name"
git config --local user.email "your@ema.il"
git config --local core.autocrlf input # For Linux/MacOS users
git config --local core.autocrlf true # For Windows users
git checkout frontend # If you're working on frontend
git checkout backend # If you're working on backend
git clone https://git.kska.io/notkshitij/Skycrate.git
```
## Where to push?
2. Change into the directory:
- For frontend, please push to [Frontend](https://github.com/kshitij-ka/cc-mini/tree/frontend/Frontend) folder in the [frontend branch](https://github.com/kshitij-ka/cc-mini/tree/frontend).
- For backend, please push to [Backend](https://github.com/kshitij-ka/cc-mini/tree/backend/Backend) folder in the [backend branch](https://github.com/kshitij-ka/cc-mini/tree/backend/).
- I will be merging changes from both the branches in the main branch for deploying.
```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`
---
+78
View File
@@ -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!
+151
View File
@@ -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
+43
View File
@@ -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/