117 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
Kshitij 1189e7cb78 Removed package-lock file. 2025-04-19 23:26:31 +05:30
Kshitij 2f6f0ba747 Removed placeholder file. 2025-04-19 23:26:12 +05:30
Kshitij 96f9ddb1d8 Merge branch 'main' into frontend. 2025-04-19 23:13:40 +05:30
Atharva Ombase 2c29597f1d {new_commit_message} 2025-04-19 20:08:15 +05:30
Atharva Ombase 1f6cbf4310 Feat: Update API URL and refactor FileTable component for improved path handling 2025-04-19 20:04:57 +05:30
Atharva Ombase afccce0be3 Feat:Added the upload reducer in the store for re-renders 2025-04-19 18:56:57 +05:30
Atharva Ombase 96fc18ab80 Fix:Removed this unused file 2025-04-19 18:56:22 +05:30
Atharva Ombase febde7dffe Feat:Called UploadStatusSlice for toggling the variable for the re-render 2025-04-19 18:54:38 +05:30
Atharva Ombase c426fecf43 Feat:Added the isUploading in the useEffect for re-rendering file table when new file/folder is uploaded 2025-04-19 18:53:34 +05:30
Atharva Ombase 0adc932e53 Feat:Added slice to re-render file table on every file uplaod and folder creation 2025-04-19 18:52:33 +05:30
Atharva Ombase 54dd5a1fcc Feat:Added logic to redirect the user to login page if user is not logged in 2025-04-19 01:29:07 +05:30
Atharva Ombase d59e8c789c Minor changes 2025-04-19 01:28:06 +05:30
Atharva Ombase 920c793fa6 Feat: Added the logic to save the username and token into the localhost alongside the expiryId. Add Login page component in Authentication module 2025-04-19 01:25:07 +05:30
Atharva Ombase 608435b758 Fix:Fixed the logic because the api endpoints were changed 2025-04-19 01:21:03 +05:30
Atharva Ombase c55dd4b661 Fix:Fixed the bug where dropdown menu was not appearing and removed uneccessary options 2025-04-19 01:19:35 +05:30
Atharva Ombase 5c9e8fedbc Feat:Added username from localhost for fetching 2025-04-19 01:18:21 +05:30
Atharva Ombase 6294066ea7 Fix:Installed axios 2025-04-18 18:11:43 +05:30
vedang29 76f9b00624 Implemented sign in & sign up (with token storage) 2025-04-18 16:33:40 +05:30
Atharva Ombase 5391410609 Fix:Fixed api call for donwloading files 2025-04-16 15:47:11 +05:30
Atharva Ombase a91d7fe8c7 Fix: Removed extra landingpage on the src/pages path 2025-04-16 15:45:50 +05:30
Atharva Ombase 65ca53b224 Fix:Changed project name 2025-04-16 15:43:02 +05:30
Atharva Ombase 9d6387699e Feat:Added provider for the redux 2025-04-16 15:41:07 +05:30
Atharva Ombase f6371faf9a Fix:Changed name of the project 2025-04-16 15:40:14 +05:30
Atharva Ombase 9632450d16 Feat:Added logic to upload the file to the Skycrate 2025-04-16 15:36:49 +05:30
Atharva Ombase e59784bfa9 Feat:Added slice for redux for hdfspath 2025-04-16 15:28:20 +05:30
Atharva Ombase 6aae767aa6 Fix:Changed title of the project 2025-04-16 15:25:40 +05:30
Atharva Ombase df4d3c1990 Fix:deleted api.js and FileUpload.jsx and made new files as needed 2025-04-16 15:25:04 +05:30
Atharva Ombase 4ab49db6af Feat:Added redux for the hdfspath to change the uplaod the file onto folder 2025-04-16 15:22:12 +05:30
Atharva Ombase 00f6e28207 Feat:Added file icons and fixed download errors 2025-04-16 15:19:55 +05:30
Atharva Ombase 1f40b02346 Feat:Added uploadFile.jsx for in UserPages 2025-04-16 15:15:05 +05:30
Kshitij 8c67d9d4c6 Meged Ombase's and Salvi's changes from main branch in frontend. 2025-04-15 11:10:35 +05:30
Kshitij 70f654179a Merged Ombase's file list in main branch with Salvi's file upload. 2025-04-15 11:07:44 +05:30
Kshitij a5168a282b Merged Salvi's upload functionality and Ombase's list files functionality. 2025-04-15 11:04:40 +05:30
Atharva Ombase 4219570d80 Feat:Added file table on the Dashboard 2025-04-15 04:14:00 +05:30
Atharva Ombase 710a08c868 Feat:Added some dependancies 2025-04-15 04:12:02 +05:30
Atharva Ombase 2f6dcf13f7 Feat:Added some dependancies 2025-04-15 04:11:16 +05:30
Atharva Ombase 8dec47ff63 Feat: Added table to list and view HDFS files with dynamic navigation 2025-04-15 04:10:27 +05:30
KaranSalvi b42a53e99b feat:Added upload file functionality 2025-04-15 03:41:16 +05:30
Kshitij 9e68d73cf4 Merged Atharv Ombase's changes from frontend branch into the main branch. 2025-03-29 22:28:10 +05:30
atharvaombase 2ae2002713 Fix:Increased the size of the eye icon in the password field. 2025-03-28 11:03:37 +05:30
atharvaombase 4457823342 Fix:Removed Google login button 2025-03-28 11:01:25 +05:30
atharvaombase 1142ece2fd Feat:Added Link for the Login and signin 2025-03-28 10:48:32 +05:30
atharvaombase 6916142deb Feat:Added redux dependencies 2025-03-28 10:21:07 +05:30
atharvaombase c798c53dcc Feat:Added new store folder for redux 2025-03-28 10:20:28 +05:30
Kshitij 596c6bf573 Added new landing page in sample, designed by Dinesh! 2025-02-27 23:17:50 +05:30
Kshitij 65783c1ca0 Merged changes from frontend branch, i.e. commits made by Ombase, Dinesh and me in frontend. 2025-02-27 23:06:51 +05:30
Kshitij 8c070fd616 Added new dashboard in sample, designed by Dinesh! 2025-02-27 23:00:46 +05:30
Kshitij 95995d0a4e Merge branch 'dinesh' into frontend. 2025-02-27 22:52:42 +05:30
Kshitij 72c1968c67 Removed package-lock from Dinesh's commit. 2025-02-27 22:51:29 +05:30
Dinesh065 000ae78d1e Improved the landing page and footer 2025-02-27 22:42:14 +05:30
Kshitij f4ac9a4f15 Removed package-lock from repo. No longer tracking it. 2025-02-26 20:18:16 +05:30
Kshitij 7c7d8a6c3a Manually merging Ombase's changes to NotFoundPage.jsx since I rebased an older commit to add a more descriptive comment. 2025-02-26 20:13:58 +05:30
Kshitij 0fe8077f7e Fix: Changed 404 image in NotFoundPage.jsx 2025-02-26 20:09:52 +05:30
Kshitij a3ab6731c6 Added package-lock to gitignore and added server in vite.config.js 2025-02-26 20:09:37 +05:30
atharvaombase c7697a4b3c Fix:Fixed 404.png import path 2025-02-26 20:08:59 +05:30
Kshitij 7994f2768d Fix: Changed 404 image in NotFoundPage.jsx 2025-02-26 20:01:48 +05:30
Kshitij bb15fe9b7a Added package-lock to gitignore 2025-02-26 19:43:02 +05:30
Kshitij 785eb9e66a Added dashboard sample image & updated README. 2025-02-26 19:07:25 +05:30
Kshitij 7590e81f5c Merged Ombase's changes from frontend branch with main branch. Created sidebar, dashboard, and not found page. Minor changes to landing page. 2025-02-26 19:00:19 +05:30
atharvaombase 8ba3d6e093 Fix:Fixed height of first page where the height was dynamic 2025-02-26 18:04:06 +05:30
atharvaombase 88cd92a159 Fix:Added route for Custom NotFoundPage 2025-02-26 18:03:05 +05:30
atharvaombase 9c5a000555 Feat:Custom Page not found page 2025-02-26 18:02:16 +05:30
atharvaombase 130cada092 Fix:Kept the light theme and removed black theme 2025-02-26 18:01:36 +05:30
atharvaombase 7f6e377674 Feat:Added searchBar in navbar 2025-02-26 16:47:43 +05:30
atharvaombase fe06abadcf Feat:Added the icons and necessary buttons 2025-02-26 15:21:15 +05:30
atharvaombase 00eab70d71 Merge branch 'frontend' of https://github.com/kshitij-ka/cc-mini into frontend 2025-02-26 12:14:50 +05:30
atharvaombase 2e74a8b762 Moved DrivethruLandingPage.jsx to its new location 2025-02-26 12:12:37 +05:30
atharvaombase e3f2d4fd0a Feat:Custom css for the Overflow scrollbar in the sidebar 2025-02-26 12:10:28 +05:30
Kshitij ddb39e4258 Merged Ombase's changes in main branch from frontend branch.
- Landing page
- Login page
- Sign-up page
2025-02-26 12:06:47 +05:30
atharvaombase 6c0b39ddbf Fix:Moved user related pages to UserPages 2025-02-26 12:06:10 +05:30
Kshitij b14c132808 Fix: Moved design link above sample images. 2025-02-26 12:04:54 +05:30
atharvaombase a7b5c24d72 Feat:Created sidebar 2025-02-26 12:04:41 +05:30
Kshitij 2556843ef6 Misc: Added sample images for landing, login and signup page. Also, referenced in README. 2025-02-26 12:03:18 +05:30
atharvaombase 43f4a8f9ce Feat:Added routes 2025-02-26 12:00:49 +05:30
atharvaombase 9abd6c554a Fix:Removed files from Root folder to Frontend folder
Feat:Created landing page,Login page, Signup page
2025-02-26 10:46:33 +05:30
atharvaombase 1005852091 Initail Commit 2025-02-25 23:39:31 +05:30
Kshitij 61a36fc2bc Added link to design in README file. 2025-02-25 01:40:02 +05:30
Kshitij 7e02ff5ba0 Added git config and branch info in readme. 2025-02-24 23:26:32 +05:30
Kshitij 714ece9637 Added a placeholder file to push Frontend folder. 2025-02-24 23:15:18 +05:30
Kshitij b2ba415373 Initial commit in frontend branch. 2025-02-24 23:04:49 +05:30
Kshitij 4f8d15836d Added info file containing info, duh! 2025-02-21 22:39:56 +05:30
Kshitij 895872c0bd Initial commit. 2025-02-21 22:31:24 +05:30
74 changed files with 2951 additions and 1280 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>
-14
View File
@@ -1,14 +0,0 @@
{
"configurations": [
{
"type": "java",
"name": "Spring Boot-MyserverApplication<myserver>",
"request": "launch",
"cwd": "${workspaceFolder}",
"mainClass": "com.example.myserver.MyserverApplication",
"projectName": "myserver",
"args": "",
"envFile": "${workspaceFolder}/.env"
}
]
}
+29
View File
@@ -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.
---
Submodule
+1
Submodule Backend added at 2622667de4
-55
View File
@@ -1,55 +0,0 @@
# API Testing Instructions (Postman)
## Signup
**Endpoint:**
```
POST http://localhost:8081/api/signup
```
**Request Body (JSON):**
```json
{
"firstname": "L",
"lastname": "H10",
"email": "exmaple@gmail.com",
"password": "your_password_here"
}
```
**Expected Response:**
```json
{
"id": 2,
"username": "exmaple@gmail.com",
"password": "hashed_password_here",
"createdAt": "your_creation_date_here",
"enabled": true,
"authorities": [],
"credentialsNonExpired": true,
"accountNonExpired": true,
"accountNonLocked": true
}
```
---
## Login
**Endpoint:**
```
POST http://localhost:8081/api/login
```
**Expected Response:**
```json
{
"token": "your_token_here",
"expiresIn": 3600000
}
```
### Notes:
- Ensure the server is running before making requests.
- Use Postman to send POST requests with the specified JSON body.
- The token received in the login response can be used for authenticated requests.
-2
View File
@@ -1,2 +0,0 @@
/mvnw text eol=lf
*.cmd text eol=crlf
-33
View File
@@ -1,33 +0,0 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
-19
View File
@@ -1,19 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
wrapperVersion=3.3.2
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
-259
View File
@@ -1,259 +0,0 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.3.2
#
# Optional ENV vars
# -----------------
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
# MVNW_REPOURL - repo url base for downloading maven distribution
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
# ----------------------------------------------------------------------------
set -euf
[ "${MVNW_VERBOSE-}" != debug ] || set -x
# OS specific support.
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
native_path() { cygpath --path --windows "$1"; }
;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
if [ -n "${JAVA_HOME-}" ]; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACCMD="$JAVA_HOME/jre/sh/javac"
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACCMD="$JAVA_HOME/bin/javac"
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
return 1
fi
fi
else
JAVACMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v java
)" || :
JAVACCMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v javac
)" || :
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
return 1
fi
fi
}
# hash string like Java String::hashCode
hash_string() {
str="${1:-}" h=0
while [ -n "$str" ]; do
char="${str%"${str#?}"}"
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
str="${str#?}"
done
printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
printf %s\\n "$1" >&2
exit 1
}
trim() {
# MWRAPPER-139:
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
# Needed for removing poorly interpreted newline sequences when running in more
# exotic environments such as mingw bash on Windows.
printf "%s" "${1}" | tr -d '[:space:]'
}
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
case "${key-}" in
distributionUrl) distributionUrl=$(trim "${value-}") ;;
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
esac
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
*)
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
distributionPlatform=linux-amd64
;;
esac
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
distributionUrlName="${distributionUrl##*/}"
distributionUrlNameMain="${distributionUrlName%.*}"
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
exec_maven "$@"
fi
case "${distributionUrl-}" in
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
trap clean HUP INT TERM EXIT
else
die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
# Download and Install Apache Maven
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
verbose "Downloading from: $distributionUrl"
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
distributionUrl="${distributionUrl%.zip}.tar.gz"
distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
# normalize http auth
case "${MVNW_PASSWORD:+has-password}" in
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
verbose "Found wget ... using wget"
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
verbose "Found curl ... using curl"
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
verbose "Falling back to use Java to download"
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
cat >"$javaSource" <<-END
public class Downloader extends java.net.Authenticator
{
protected java.net.PasswordAuthentication getPasswordAuthentication()
{
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
}
public static void main( String[] args ) throws Exception
{
setDefault( new Downloader() );
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
}
}
END
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
verbose " - Compiling Downloader.java ..."
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
verbose " - Running Downloader.java ..."
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
distributionSha256Result=false
if [ "$MVN_CMD" = mvnd.sh ]; then
echo "Checksum validation is not supported for maven-mvnd." >&2
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
elif command -v sha256sum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
elif command -v shasum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
fi
if [ $distributionSha256Result = false ]; then
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
exit 1
fi
fi
# unzip and move
if command -v unzip >/dev/null; then
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
fi
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"
-149
View File
@@ -1,149 +0,0 @@
<# : batch portion
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.2
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
if ($env:MAVEN_USER_HOME) {
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
}
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
-96
View File
@@ -1,96 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>myserver</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>myserver</name>
<description>CC mini project</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -1,14 +0,0 @@
package com.example.myserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
@SpringBootApplication
public class MyserverApplication {
public static void main(String[] args) {
SpringApplication.run(MyserverApplication.class, args);
}
}
@@ -1,47 +0,0 @@
package com.example.myserver.configs;
import java.security.AuthProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import com.example.myserver.repositories.UserRepository;
@Configuration
public class ApplicationConfiguration {
private final UserRepository userRepository;
public ApplicationConfiguration(UserRepository userRepository){
this.userRepository=userRepository;
}
@Bean
UserDetailsService userDetailsService() {
return username -> userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
@Bean
BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception{
return config.getAuthenticationManager();
}
@Bean
AuthenticationProvider authenticationProvider(){
DaoAuthenticationProvider authprovider=new DaoAuthenticationProvider();
authprovider.setUserDetailsService(userDetailsService());
authprovider.setPasswordEncoder(passwordEncoder());
return authprovider;
}
}
@@ -1,77 +0,0 @@
package com.example.myserver.configs;
import java.io.IOException;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.HandlerExceptionResolver;
import com.example.myserver.services.JwtService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final HandlerExceptionResolver handlerExceptionResolver;
private JwtService jwtService;
private UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtService jwtService,UserDetailsService userDetailsService,HandlerExceptionResolver handlerExceptionResolver){
this.handlerExceptionResolver=handlerExceptionResolver;
this.jwtService=jwtService;
this.userDetailsService=userDetailsService;
}
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
final String authHeader=request.getHeader("Authorization");
if (authHeader==null || !authHeader.startsWith("Bearer")){
filterChain.doFilter(request, response);
return;
}
try {
final String userjwt=authHeader.substring(7);
final String userEmail=jwtService.extractUsername(userjwt);
Authentication authentication=SecurityContextHolder.getContext().getAuthentication();
if(userEmail!=null && authentication==null){
UserDetails userDetails=this.userDetailsService.loadUserByUsername(userEmail);
if (jwtService.isTokenValid(userjwt, userDetails)) {
UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
filterChain.doFilter(request, response);
}
catch (Exception err) {
handlerExceptionResolver.resolveException(request, response, null, err);
}
}
}
@@ -1,63 +0,0 @@
package com.example.myserver.configs;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
private final AuthenticationProvider authenticationProvider;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfiguration(
JwtAuthenticationFilter jwtAuthenticationFilter,
AuthenticationProvider authenticationProvider
) {
this.authenticationProvider = authenticationProvider;
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.authorizeHttpRequests()
.requestMatchers("/api/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("*"));
configuration.setAllowedMethods(List.of("GET","PUT","DELETE","POST"));
configuration.setAllowedHeaders(List.of("Authorization","Content-Type"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**",configuration);
return source;
}
}
@@ -1,58 +0,0 @@
package com.example.myserver.controllers;
import org.springframework.web.bind.annotation.RestController;
import com.example.myserver.dtos.LoginUserDto;
import com.example.myserver.dtos.RegisterUserDto;
import com.example.myserver.models.User;
import com.example.myserver.responses.LoginResponse;
import com.example.myserver.services.AuthenticationService;
import com.example.myserver.services.JwtService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@RequestMapping("/api")
@RestController
public class AuthController {
private final JwtService jwtService;
private AuthenticationService authenticationService;
public AuthController(JwtService jwtService,AuthenticationService authenticationService){
this.jwtService=jwtService;
this.authenticationService=authenticationService;
}
@GetMapping("/test")
public String teString(@RequestParam String param) {
return new String();
}
@PostMapping("/login")
public ResponseEntity<LoginResponse> LoginController(@RequestBody LoginUserDto entity) {
User authenticatedUser=authenticationService.authenticate(entity);
String jwtToken=jwtService.generateToken(authenticatedUser);
LoginResponse loginResponse=new LoginResponse().setToken(jwtToken).setExpiresIn(jwtService.getExpirtationTime());
return ResponseEntity.ok(loginResponse);
}
@PostMapping("/signup")
public ResponseEntity<User> register(@RequestBody RegisterUserDto entity) {
User registeredUser=authenticationService.signUp(entity);
return ResponseEntity.ok(registeredUser);
}
}
@@ -1,25 +0,0 @@
package com.example.myserver.dtos;
public class LoginUserDto {
private String email;
private String password;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
@@ -1,46 +0,0 @@
package com.example.myserver.dtos;
public class RegisterUserDto {
private String email;
private String password;
private String firstname;
private String lastname;
public String getEmail() {
return email;
}
public RegisterUserDto setEmail(String email) {
this.email = email;
return this;
}
public String getPassword() {
return password;
}
public RegisterUserDto setPassword(String password) {
this.password = password;
return this;
}
public String getFirstname() {
return firstname;
}
public RegisterUserDto setFirstname(String firstname) {
this.firstname = firstname;
return this;
}
public String getLastname() {
return lastname;
}
public RegisterUserDto setLastname(String lastname) {
this.lastname = lastname;
return this;
}
}
@@ -1,128 +0,0 @@
package com.example.myserver.models;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import jakarta.persistence.*;
@Table(name = "users")
@Entity
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false)
private Integer id;
@Column(nullable = false)
private String username;
/*
//Optional feature might add later
@Column(name = "verification_code")
private String verificationCode;
@Column(name ="verification_expiry")
private LocalDateTime verificationExpiry;
*/
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
public User(){
}
public User(String firstname,String lastname,String email,String password){
this.username=firstname+lastname;
this.email=email;
this.password=password;
}
@CreationTimestamp
@Column(updatable = false, name = "created_at")
private Date createdAt;
@Override
public Collection<? extends GrantedAuthority> getAuthorities(){
return List.of();
}
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public void setFullname(String firstname,String lastname) {
this.username=firstname+lastname;
}
public String getFullname(String firstname,String lastname){
return this.username;
}
public void setEmail(String email) {
this.email = email;
}
public void setPassword(String password) {
this.password = password;
}
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
}
@@ -1,13 +0,0 @@
package com.example.myserver.repositories;
import java.util.Optional;
import org.springframework.data.repository.CrudRepository;
import com.example.myserver.models.User;
public interface UserRepository extends CrudRepository<User,Integer> {
Optional<User> findByEmail(String email);
/*
// might use later
Optional<User> findByVerificationCode(String verificationCode);
*/
}
@@ -1,25 +0,0 @@
package com.example.myserver.responses;
public class LoginResponse {
private String token;
private long expiresIn;
public String getToken() {
return token;
}
public LoginResponse setToken(String token) {
this.token = token;
return this;
}
public long getExpiresIn() {
return expiresIn;
}
public LoginResponse setExpiresIn(long expiresIn) {
this.expiresIn = expiresIn;
return this;
}
}
@@ -1,45 +0,0 @@
package com.example.myserver.services;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.example.myserver.dtos.LoginUserDto;
import com.example.myserver.dtos.RegisterUserDto;
import com.example.myserver.models.User;
import com.example.myserver.repositories.UserRepository;
@Service
public class AuthenticationService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
public AuthenticationService( UserRepository userRepository, AuthenticationManager authenticationManager , PasswordEncoder passwordEncoder){
this.userRepository=userRepository;
this.passwordEncoder=passwordEncoder;
this.authenticationManager=authenticationManager;
}
public User signUp(RegisterUserDto inputuser){
User user=new User(inputuser.getFirstname(),inputuser.getLastname(),inputuser.getEmail(),passwordEncoder.encode(inputuser.getPassword()));
/*
User user = new User()
.setFullname(inputuser.getFirstname(),inputuser.getLastname())
.setEmail(inputuser.getEmail())
.setPassword(passwordEncoder.encode(inputuser.getPassword()));
*/
return userRepository.save(user) ;
}
public User authenticate(LoginUserDto inputuser){
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(inputuser.getEmail()
, inputuser.getPassword()));
return userRepository.findByEmail(inputuser.getEmail()).orElseThrow();
}
}
@@ -1,83 +0,0 @@
package com.example.myserver.services;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
@Service
public class JwtService {
@Value("${security.jwt.secret-key}")
private String secretKey;
@Value("${security.jwt.expiration-time}")
private long jwtExpiration;
public String extractUsername(String token){
return extractClaim(token,Claims::getSubject);
}
public <T> T extractClaim(String token,Function<Claims,T> claimsResolver){
final Claims claims=extractAllClaims(token);
return claimsResolver.apply(claims);
}
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return buildToken(extraClaims, userDetails, jwtExpiration);
}
public long getExpirtationTime(){
return jwtExpiration;
}
private String buildToken(Map<String,Object> extraClaims,UserDetails userDetails,long expiration){
return Jwts.builder().setClaims(extraClaims).setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private Claims extractAllClaims(String token) {
return Jwts
.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
}
private Key getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}
@@ -1,14 +0,0 @@
# spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
server.port=8081
security.jwt.secret-key=3cfa76ef14937c1c0ea519f8fc057a80fcd04a7420f8e8bcd0a7567c272e007b
security.jwt.expiration-time=3600000
spring.datasource.url=jdbc:mysql://localhost:3306/drive?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=sqlsys
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.open-in-view=false
@@ -1,13 +0,0 @@
package com.example.myserver;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class MyserverApplicationTests {
@Test
void contextLoads() {
}
}
+1
View File
@@ -0,0 +1 @@
VITE_API_URL=http://localhost:8081
+29
View File
@@ -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/
+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"]
+38
View File
@@ -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 },
],
},
},
]
+22
View File
@@ -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>
+37
View File
@@ -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"
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

+25
View File
@@ -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;
+237
View File
@@ -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;
+194
View File
@@ -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;
+189
View File
@@ -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;
+155
View File
@@ -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;
+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;
+28
View File
@@ -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 */
}
+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": "आवडते"
}
+18
View File
@@ -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>
);
+187
View File
@@ -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;
+18
View File
@@ -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;
+19
View File
@@ -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;
+11
View File
@@ -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,
},
});
+16
View File
@@ -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,
},
})
+50 -2
View File
@@ -1,6 +1,54 @@
# CC-MINI (2025) # Skycrate
`backend` branch is intended only for pushing backend files within the [Backend](Backend/) folder. > [!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`
---
+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/