Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc12c47491 |
1
.gitignore
vendored
@ -1,3 +1,2 @@
|
|||||||
.env
|
.env
|
||||||
.venv
|
.venv
|
||||||
frontend
|
|
||||||
22
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
0
frontend/5.2.0
Normal file
70
frontend/README.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# Getting Started with Create React App
|
||||||
|
|
||||||
|
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.\
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||||
|
|
||||||
|
The page will reload when you make changes.\
|
||||||
|
You may also see any lint errors in the console.
|
||||||
|
|
||||||
|
### `npm test`
|
||||||
|
|
||||||
|
Launches the test runner in the interactive watch mode.\
|
||||||
|
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `build` folder.\
|
||||||
|
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.\
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||||
|
|
||||||
|
### `npm run eject`
|
||||||
|
|
||||||
|
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||||
|
|
||||||
|
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||||
|
|
||||||
|
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||||
|
|
||||||
|
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||||
|
|
||||||
|
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||||
|
|
||||||
|
### Code Splitting
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||||
|
|
||||||
|
### Analyzing the Bundle Size
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||||
|
|
||||||
|
### Making a Progressive Web App
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||||
|
|
||||||
|
### Advanced Configuration
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||||
|
|
||||||
|
### `npm run build` fails to minify
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||||
17777
frontend/package-lock.json
generated
Normal file
47
frontend/package.json
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "my-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/dom": "^10.4.1",
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.0",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"bootstrap": "^5.3.8",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"react-router-dom": "^6.30.1",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"styled-components": "^6.1.19",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^4.1.14"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
frontend/public/assets/images/2 copy.png
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
frontend/public/assets/images/2.png
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
frontend/public/assets/images/3.png
Normal file
|
After Width: | Height: | Size: 664 KiB |
BIN
frontend/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
43
frontend/public/index.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
frontend/public/logo192.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
frontend/public/logo512.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
25
frontend/public/manifest.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
||||||
3
frontend/public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
54
frontend/src/App.css
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
.App {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-logo {
|
||||||
|
height: 40vmin;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.App-logo {
|
||||||
|
animation: App-logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-header {
|
||||||
|
background-color: #282c34;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: calc(10px + 2vmin);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-link {
|
||||||
|
color: #61dafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes App-logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.toast-alert {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: -400px; /* Start off-screen */
|
||||||
|
max-width: 350px;
|
||||||
|
z-index: 1050;
|
||||||
|
transition: right 0.3s ease-out;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-alert.show {
|
||||||
|
right: 20px; /* Slide in */
|
||||||
|
}
|
||||||
34
frontend/src/App.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// src/App.jsx
|
||||||
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
|
import Login from './components/Login';
|
||||||
|
import LandingPage from './components/LandingPage';
|
||||||
|
import VerifyEmail from './components/VerifyEmail';
|
||||||
|
import Navbar from './components/Navbar';
|
||||||
|
import Dashboard from './pages/Dashboard';
|
||||||
|
import ResetPassword from './components/ResetPassword';
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<div className="App">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path='/reset-password' element={<ResetPassword />} />
|
||||||
|
<Route path="/verify-email" element={<VerifyEmail />} />
|
||||||
|
<Route path="/dashboard" element={<Dashboard/>} />
|
||||||
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={
|
||||||
|
<>
|
||||||
|
{/* <Navbar />
|
||||||
|
<LandingPage /> */}
|
||||||
|
< Login />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
8
frontend/src/App.test.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
test('renders learn react link', () => {
|
||||||
|
render(<App />);
|
||||||
|
const linkElement = screen.getByText(/learn react/i);
|
||||||
|
expect(linkElement).toBeInTheDocument();
|
||||||
|
});
|
||||||
44
frontend/src/components/LandingPage.jsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
export default function LandingPage() {
|
||||||
|
return (
|
||||||
|
<div className="vh-100 bg-light d-flex flex-column">
|
||||||
|
<main className="flex-grow-1 d-flex align-items-center justify-content-center p-4">
|
||||||
|
<div className="text-center" style={{ maxWidth: '600px' }}>
|
||||||
|
<h1 className="fw-bold display-5 mb-3" style={{ color: '#333' }}>
|
||||||
|
Your Personal <br />
|
||||||
|
<span className="text-primary" style={{color:'#00b4d8'}}>AI Doc/PDF</span> <br />
|
||||||
|
Generator
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted mb-4" style={{ fontSize: '0.9rem' }}>
|
||||||
|
Generate professional documents quickly and efficiently with AI.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="d-flex flex-wrap gap-3 justify-content-center">
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/login"
|
||||||
|
className="btn btn-lg px-4 rounded-pill fw-bold"
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#00b4d8',
|
||||||
|
borderColor: '#00b4d8',
|
||||||
|
color: 'white',
|
||||||
|
textDecoration: 'none', // removes underline
|
||||||
|
display: 'inline-block', // ensures button-like behavior
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.target.style.backgroundColor = '#009ecf';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.target.style.backgroundColor = '#00b4d8';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Log in →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
304
frontend/src/components/Login.jsx
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import '../App.css';
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [rememberMe, setRememberMe] = useState(false);
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
// Toast state: array of toasts
|
||||||
|
const [toasts, setToasts] = useState([]);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Load attempt count from localStorage on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const saved = localStorage.getItem('loginAttempts');
|
||||||
|
if (saved) {
|
||||||
|
const attempts = JSON.parse(saved);
|
||||||
|
const now = Date.now();
|
||||||
|
const validAttempts = {};
|
||||||
|
for (const [key, value] of Object.entries(attempts)) {
|
||||||
|
if (now - value.timestamp < 3600000) {
|
||||||
|
validAttempts[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
localStorage.setItem('loginAttempts', JSON.stringify(validAttempts));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getAttemptCount = (email) => {
|
||||||
|
const attempts = JSON.parse(localStorage.getItem('loginAttempts') || '{}');
|
||||||
|
return attempts[email]?.count || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const incrementAttempt = (email) => {
|
||||||
|
const attempts = JSON.parse(localStorage.getItem('loginAttempts') || '{}');
|
||||||
|
const now = Date.now();
|
||||||
|
attempts[email] = {
|
||||||
|
count: (attempts[email]?.count || 0) + 1,
|
||||||
|
timestamp: now,
|
||||||
|
};
|
||||||
|
localStorage.setItem('loginAttempts', JSON.stringify(attempts));
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearAttempts = (email) => {
|
||||||
|
const attempts = JSON.parse(localStorage.getItem('loginAttempts') || '{}');
|
||||||
|
delete attempts[email];
|
||||||
|
localStorage.setItem('loginAttempts', JSON.stringify(attempts));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add toast
|
||||||
|
// Add toast with 5-second timer
|
||||||
|
const addToast = (message, type = 'danger') => {
|
||||||
|
const id = Date.now();
|
||||||
|
const duration = 5000; // 5 seconds
|
||||||
|
|
||||||
|
// Add toast
|
||||||
|
setToasts(prev => [
|
||||||
|
...prev,
|
||||||
|
{ id, message, type, createdAt: Date.now(), duration, progress: 100 }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Start interval to update progress every 50ms
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setToasts(prev =>
|
||||||
|
prev.map(toast =>
|
||||||
|
toast.id === id
|
||||||
|
? {
|
||||||
|
...toast,
|
||||||
|
progress: Math.max(0, toast.progress - (30 / duration) * 100)
|
||||||
|
}
|
||||||
|
: toast
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, 30);
|
||||||
|
|
||||||
|
// Auto-dismiss after duration
|
||||||
|
setTimeout(() => {
|
||||||
|
clearInterval(interval);
|
||||||
|
setToasts(prev => prev.filter(t => t.id !== id));
|
||||||
|
}, duration);
|
||||||
|
};
|
||||||
|
// Remove toast by ID
|
||||||
|
const removeToast = (id) => {
|
||||||
|
setToasts(prev => prev.filter(t => t.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
setError('Please fill in all fields');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attemptCount = getAttemptCount(email);
|
||||||
|
if (attemptCount >= 6) {
|
||||||
|
addToast(`You've exceeded login attempts for ${email}. Please reset your password.`, 'danger');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 🔌 REPLACE WITH YOUR LOGIN API
|
||||||
|
/*
|
||||||
|
const response = await fetch('/api/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, password, rememberMe }),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.message || 'Invalid credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success: clear attempts, save token, redirect
|
||||||
|
clearAttempts(email);
|
||||||
|
localStorage.setItem('token', data.token);
|
||||||
|
navigate('/dashboard');
|
||||||
|
return;
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 🧪 Simulate: let's say "test@example.com" / "123456" works
|
||||||
|
if (email === 'test@example.com' && password === '123456') {
|
||||||
|
clearAttempts(email);
|
||||||
|
alert('✅ Login successful!');
|
||||||
|
navigate('/dashboard');
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid email or password');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
incrementAttempt(email);
|
||||||
|
const newCount = getAttemptCount(email);
|
||||||
|
|
||||||
|
if (newCount >= 6) {
|
||||||
|
addToast(`You've exceeded login attempts for ${email}. Please reset your password.`, 'danger');
|
||||||
|
setError('');
|
||||||
|
} else {
|
||||||
|
setError(`${err.message} (${6 - newCount} attempts left)`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Background & Overlay */}
|
||||||
|
<div className="position-absolute top-0 start-0 w-100 h-100" style={{ backgroundImage: 'url(/assets/images/3.png)', backgroundSize: 'cover', backgroundPosition: 'center' }}></div>
|
||||||
|
{/* <div className="position-absolute top-0 start-0 w-100 h-100 bg-white opacity-75"></div> */}
|
||||||
|
|
||||||
|
{/* Toast Stack (Right Side) */}
|
||||||
|
{/* Toast Stack (Right Side) */}
|
||||||
|
<div className="position-fixed top-4 end-4 d-flex flex-column gap-2" style={{ zIndex: 1050 }}>
|
||||||
|
{toasts.map(toast => {
|
||||||
|
// Calculate progress percentage (0% to 100%)
|
||||||
|
const elapsed = Date.now() - toast.createdAt;
|
||||||
|
const progress = Math.max(0, 100 - (elapsed / toast.duration) * 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={toast.id}
|
||||||
|
className={`alert alert-${toast.type} p-3 rounded shadow-sm position-relative`}
|
||||||
|
style={{
|
||||||
|
width: '300px',
|
||||||
|
borderLeft: `4px solid ${toast.type === 'danger' ? '#dc3545' : '#ffc107'}`,
|
||||||
|
animation: 'slideInRight 0.3s ease-out',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="d-flex justify-content-between align-items-start">
|
||||||
|
<div className="d-flex align-items-center gap-2">
|
||||||
|
<span style={{ fontSize: '1.1rem' }}>
|
||||||
|
{toast.type === 'danger' ? '⚠️' : 'ℹ️'}
|
||||||
|
</span>
|
||||||
|
<p className="mb-0 small">{toast.message}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-close"
|
||||||
|
onClick={() => removeToast(toast.id)}
|
||||||
|
style={{ fontSize: '0.8rem' }}
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timer Progress Bar */}
|
||||||
|
<div className="mt-2" style={{ height: '4px', backgroundColor: '#e9ecef', borderRadius: '2px', overflow: 'hidden' }}>
|
||||||
|
<div
|
||||||
|
className="h-100"
|
||||||
|
style={{
|
||||||
|
width: `${progress}%`,
|
||||||
|
backgroundColor: toast.type === 'danger' ? '#dc3545' : '#ffc107',
|
||||||
|
transition: 'width 0.1s linear',
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{/* Login Card */}
|
||||||
|
<div className="position-relative d-flex align-items-center justify-content-center min-vh-100 p-3">
|
||||||
|
<div className="card shadow-sm border-0 rounded-4 p-4 w-100" style={{ maxWidth: '420px' }}>
|
||||||
|
<div className="text-center mb-3">
|
||||||
|
<h4 className="text-info fw-bold">Lolita</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="text-start">
|
||||||
|
<h5 className="mb-2 text-center">Sign in to account</h5>
|
||||||
|
<p className="text-muted mb-4 small text-center">Enter your email & password to login</p>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="form-label small">Email Address</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className="form-control"
|
||||||
|
style={{ borderColor: '#e0e0e0', backgroundColor: '#f0f8fa' }}
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="Enter email"
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="form-label small">Password</label>
|
||||||
|
<div className="input-group">
|
||||||
|
<input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
className="form-control"
|
||||||
|
style={{ borderColor: '#e0e0e0', backgroundColor: '#f0f8fa' }}
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-secondary btn-sm"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{showPassword ? 'hide' : 'show'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="alert alert-danger small p-2 mb-3">{error}</div>}
|
||||||
|
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div className="form-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
id="rememberMe"
|
||||||
|
checked={rememberMe}
|
||||||
|
onChange={(e) => setRememberMe(e.target.checked)}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<label className="form-check-label small" htmlFor="rememberMe">
|
||||||
|
Remember me
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<Link to="/reset-password" className="text-decoration-none small" style={{ color: '#ff5757' }}>
|
||||||
|
Forgot password?
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn w-100 py-2 fw-medium"
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#00b4d8',
|
||||||
|
borderColor: '#00b4d8',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? 'Signing in...' : 'Log in'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Slide-in Animation */}
|
||||||
|
<style jsx>{`
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from { transform: translateX(100%); opacity: 0; }
|
||||||
|
to { transform: translateX(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
frontend/src/components/Navbar.jsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
const Navbar = () => {
|
||||||
|
return (
|
||||||
|
<nav className="navbar navbar-expand-lg px-4 py-3 bg-white shadow-sm">
|
||||||
|
<div className="container-fluid">
|
||||||
|
{/* Brand - teal color */}
|
||||||
|
<Link
|
||||||
|
className="navbar-brand fw-bold fs-4"
|
||||||
|
to="/"
|
||||||
|
style={{ color: '#00b4d8' }} // 👈 brand color
|
||||||
|
>
|
||||||
|
Lolita
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="navbar-toggler"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#navbarNav"
|
||||||
|
aria-controls="navbarNav"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-label="Toggle navigation"
|
||||||
|
>
|
||||||
|
<span className="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul className="navbar-nav ms-auto align-items-lg-center">
|
||||||
|
<li className="nav-item">
|
||||||
|
<Link
|
||||||
|
className="nav-link small"
|
||||||
|
to="/support"
|
||||||
|
style={{ color: '#00b4d8' }} // 👈 link color
|
||||||
|
>
|
||||||
|
Support
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<Link
|
||||||
|
className="nav-link small"
|
||||||
|
to="/about"
|
||||||
|
style={{ color: '#00b4d8' }}
|
||||||
|
>
|
||||||
|
About us
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Navbar;
|
||||||
423
frontend/src/components/ResetPassword.jsx
Normal file
@ -0,0 +1,423 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import '../App.css';
|
||||||
|
|
||||||
|
// 🔑 Configurable OTP expiry time (in seconds)
|
||||||
|
const OTP_EXPIRY_SECONDS = 300; // 5 minutes
|
||||||
|
|
||||||
|
export default function ResetPassword() {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [otp, setOtp] = useState(['', '', '', '', '', '']);
|
||||||
|
const [newPassword, setNewPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [rememberMe, setRememberMe] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
const [isOtpExpired, setIsOtpExpired] = useState(false);
|
||||||
|
const [countdown, setCountdown] = useState(0);
|
||||||
|
const countdownInterval = useRef(null);
|
||||||
|
|
||||||
|
const otpInputRefs = useRef([]);
|
||||||
|
|
||||||
|
// 🔑 Password rules
|
||||||
|
const passwordRules = [
|
||||||
|
{ id: 'length', regex: /.{8,}/, label: 'At least 8 characters' },
|
||||||
|
{ id: 'uppercase', regex: /[A-Z]/, label: 'One uppercase letter' },
|
||||||
|
{ id: 'lowercase', regex: /[a-z]/, label: 'One lowercase letter' },
|
||||||
|
{ id: 'number', regex: /[0-9]/, label: 'One number' },
|
||||||
|
{ id: 'special', regex: /[^A-Za-z0-9]/, label: 'One special character (e.g., !@#$%)' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const isPasswordValid = (password) => {
|
||||||
|
return passwordRules.every(rule => rule.regex.test(password));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🔑 Start countdown timer
|
||||||
|
const startCountdown = (seconds) => {
|
||||||
|
if (countdownInterval.current) {
|
||||||
|
clearInterval(countdownInterval.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCountdown(seconds);
|
||||||
|
|
||||||
|
countdownInterval.current = setInterval(() => {
|
||||||
|
setCountdown(prev => {
|
||||||
|
if (prev <= 1) {
|
||||||
|
clearInterval(countdownInterval.current);
|
||||||
|
setIsOtpExpired(true);
|
||||||
|
localStorage.removeItem('otpData');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🔑 On mount: check for existing OTP and resume countdown
|
||||||
|
useEffect(() => {
|
||||||
|
const stored = localStorage.getItem('otpData');
|
||||||
|
if (stored) {
|
||||||
|
const { timestamp } = JSON.parse(stored);
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsedMs = now - timestamp;
|
||||||
|
const remainingMs = OTP_EXPIRY_SECONDS * 1000 - elapsedMs;
|
||||||
|
|
||||||
|
if (remainingMs > 0) {
|
||||||
|
const remainingSeconds = Math.ceil(remainingMs / 1000);
|
||||||
|
startCountdown(remainingSeconds);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('otpData');
|
||||||
|
setIsOtpExpired(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
return () => {
|
||||||
|
if (countdownInterval.current) {
|
||||||
|
clearInterval(countdownInterval.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 🔑 Save OTP with timestamp
|
||||||
|
const saveOtpWithTimestamp = (email, otpCode) => {
|
||||||
|
localStorage.setItem('otpData', JSON.stringify({
|
||||||
|
email,
|
||||||
|
otp: otpCode,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🔑 Get saved OTP data
|
||||||
|
const getSavedOtpData = () => {
|
||||||
|
const stored = localStorage.getItem('otpData');
|
||||||
|
if (stored) {
|
||||||
|
const data = JSON.parse(stored);
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - data.timestamp <= OTP_EXPIRY_SECONDS * 1000) {
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('otpData');
|
||||||
|
setIsOtpExpired(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle OTP input
|
||||||
|
const handleOtpChange = (index, value) => {
|
||||||
|
if (/^\d?$/.test(value)) {
|
||||||
|
const newOtp = [...otp];
|
||||||
|
newOtp[index] = value;
|
||||||
|
setOtp(newOtp);
|
||||||
|
if (value !== '' && index < otp.length - 1) {
|
||||||
|
otpInputRefs.current[index + 1]?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOtpKeyDown = (index, e) => {
|
||||||
|
if (e.key === 'Backspace' && !otp[index] && index > 0) {
|
||||||
|
otpInputRefs.current[index - 1]?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOtpPaste = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const paste = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, 6);
|
||||||
|
if (paste.length === 6) {
|
||||||
|
const newOtp = paste.split('');
|
||||||
|
setOtp(newOtp);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🔑 Handle "Send OTP" click
|
||||||
|
const handleSendOtp = () => {
|
||||||
|
if (!email || !/\S+@\S+\.\S+/.test(email)) return;
|
||||||
|
|
||||||
|
console.log('Send OTP clicked for:', email);
|
||||||
|
alert('✅ Simulated "Send OTP" API call!');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const demoOtp = ['1','2','3','4','5','6'];
|
||||||
|
setOtp(demoOtp);
|
||||||
|
saveOtpWithTimestamp(email, demoOtp.join(''));
|
||||||
|
startCountdown(OTP_EXPIRY_SECONDS); // ✅ Start fresh countdown
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🔑 Handle "Resend OTP" click
|
||||||
|
const handleResendOtp = () => {
|
||||||
|
if (!email || !/\S+@\S+\.\S+/.test(email)) {
|
||||||
|
setError('Please enter a valid email first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Resend OTP clicked for:', email);
|
||||||
|
alert('✅ OTP resent!');
|
||||||
|
|
||||||
|
setOtp(['', '', '', '', '', '']);
|
||||||
|
localStorage.removeItem('otpData');
|
||||||
|
setIsOtpExpired(false);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const demoOtp = ['6','5','4','3','2','1'];
|
||||||
|
setOtp(demoOtp);
|
||||||
|
saveOtpWithTimestamp(email, demoOtp.join(''));
|
||||||
|
startCountdown(OTP_EXPIRY_SECONDS); // ✅ Start fresh countdown
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
setSuccess(false);
|
||||||
|
|
||||||
|
if (!isPasswordValid(newPassword)) {
|
||||||
|
setError('Password does not meet security requirements');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
setError('Passwords do not match');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const otpCode = otp.join('');
|
||||||
|
if (otpCode.length !== 6) {
|
||||||
|
setError('Please enter a 6-digit OTP');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedOtpData = getSavedOtpData();
|
||||||
|
if (!savedOtpData) {
|
||||||
|
setError('OTP has expired or was not requested. Please request a new OTP.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedOtpData.otp !== otpCode) {
|
||||||
|
setError('Invalid OTP. Please check your code.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedOtpData.email !== email) {
|
||||||
|
setError('OTP does not match the email address.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
console.log('API Payload:', { email, otp: otpCode, newPassword, rememberMe });
|
||||||
|
alert('✅ Password reset successful!');
|
||||||
|
setSuccess(true);
|
||||||
|
localStorage.removeItem('otpData');
|
||||||
|
if (countdownInterval.current) {
|
||||||
|
clearInterval(countdownInterval.current);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || 'Something went wrong');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="position-absolute top-0 start-0 w-100 h-100"
|
||||||
|
style={{
|
||||||
|
backgroundImage: 'url(/assets/images/3.png)',
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div className="position-relative d-flex align-items-center justify-content-center min-vh-100 p-3">
|
||||||
|
<div className="card shadow-sm border-0 rounded-4 p-4 w-100" style={{ maxWidth: '400px' }}>
|
||||||
|
<div className="text-center mb-3">
|
||||||
|
<h4 className="text-info fw-bold">Lolita</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="text-start">
|
||||||
|
<h5 className="mb-2 text-center">Reset Your Password</h5>
|
||||||
|
<p className="text-muted mb-3 small mt-4">Enter your registered Email</p>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className="form-control"
|
||||||
|
style={{ borderColor: '#e0e0e0', backgroundColor: '#f0f8fa' }}
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="Enter email"
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="d-flex justify-content-end mb-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm"
|
||||||
|
style={{
|
||||||
|
backgroundColor: email && /\S+@\S+\.\S+/.test(email) ? '#00b4d8' : '#cccccc',
|
||||||
|
color: 'white',
|
||||||
|
borderColor: email && /\S+@\S+\.\S+/.test(email) ? '#00b4d8' : '#cccccc',
|
||||||
|
}}
|
||||||
|
disabled={!email || !/\S+@\S+\.\S+/.test(email) || loading}
|
||||||
|
onClick={handleSendOtp}
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 🔑 LIVE COUNTDOWN - Only shows when active */}
|
||||||
|
{countdown > 0 && (
|
||||||
|
<div className="text-muted small mb-2 text-center">
|
||||||
|
OTP expires in <strong>{countdown}s</strong>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isOtpExpired && countdown === 0 && (
|
||||||
|
<div className="alert alert-warning small p-2 mb-3">
|
||||||
|
⚠️ Your OTP has expired. Please request a new one.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="text-muted small mb-3">
|
||||||
|
Didn’t receive OTP?{' '}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-link p-0"
|
||||||
|
style={{ color: '#ff5757', textDecoration: 'none' }}
|
||||||
|
disabled={loading}
|
||||||
|
onClick={handleResendOtp}
|
||||||
|
>
|
||||||
|
Resend
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="form-label small">Enter OTP</label>
|
||||||
|
<div className="d-flex gap-2 flex-wrap mt-2">
|
||||||
|
{otp.map((digit, index) => (
|
||||||
|
<input
|
||||||
|
key={index}
|
||||||
|
ref={(el) => (otpInputRefs.current[index] = el)}
|
||||||
|
type="text"
|
||||||
|
inputMode="numeric"
|
||||||
|
className="form-control text-center"
|
||||||
|
style={{
|
||||||
|
width: '50px',
|
||||||
|
height: '50px',
|
||||||
|
fontSize: '1.2rem',
|
||||||
|
borderColor: '#e0e0e0',
|
||||||
|
backgroundColor: '#f0f8fa',
|
||||||
|
}}
|
||||||
|
value={digit}
|
||||||
|
onChange={(e) => handleOtpChange(index, e.target.value)}
|
||||||
|
onKeyDown={(e) => handleOtpKeyDown(index, e)}
|
||||||
|
onPaste={handleOtpPaste}
|
||||||
|
maxLength={1}
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Password Section */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<h6 className="fw-medium mb-2">Create Your Password</h6>
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="form-label small">New Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-control"
|
||||||
|
style={{ borderColor: '#e0e0e0', backgroundColor: '#f0f8fa' }}
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Password Rules - Only show when user types */}
|
||||||
|
{newPassword && (
|
||||||
|
<div className="small mb-3">
|
||||||
|
{passwordRules.map((rule) => {
|
||||||
|
const isValid = rule.regex.test(newPassword);
|
||||||
|
return (
|
||||||
|
<div key={rule.id} className="d-flex align-items-center mb-1">
|
||||||
|
<span className="me-2" style={{ fontSize: '0.9rem' }}>
|
||||||
|
{isValid ? '✅' : '❌'}
|
||||||
|
</span>
|
||||||
|
<span className={isValid ? 'text-success' : 'text-muted'}>
|
||||||
|
{rule.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="form-label small">Retype Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-control"
|
||||||
|
style={{ borderColor: '#e0e0e0', backgroundColor: '#f0f8fa' }}
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-check mb-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
id="rememberMe"
|
||||||
|
checked={rememberMe}
|
||||||
|
onChange={(e) => setRememberMe(e.target.checked)}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<label className="form-check-label small" htmlFor="rememberMe">
|
||||||
|
Remember password
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="alert alert-danger small p-2 mb-3">{error}</div>}
|
||||||
|
{success && <div className="alert alert-success small p-2 mb-3">Password updated successfully!</div>}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn w-100 py-2 fw-medium"
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#00b4d8',
|
||||||
|
borderColor: '#00b4d8',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
disabled={loading || !isPasswordValid(newPassword) || !confirmPassword}
|
||||||
|
>
|
||||||
|
{loading ? 'Processing...' : 'Done'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="text-center mt-3">
|
||||||
|
<Link to="/login" className="text-decoration-none small" style={{ color: '#00b4d8' }}>
|
||||||
|
← Back to Login
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
126
frontend/src/components/Signup.jsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import Navbar from './Navbar';
|
||||||
|
|
||||||
|
|
||||||
|
export default function Signup() {
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [phone, setPhone] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
alert("Passwords don't match!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log({ name, email, phone, password });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
<div className="vh-100 bg-light d-flex flex-column">
|
||||||
|
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="flex-grow-1 d-flex align-items-center justify-content-center p-4">
|
||||||
|
<div className="w-100" style={{ maxWidth: '480px' }}>
|
||||||
|
<div className="card border-0 shadow-lg rounded-4">
|
||||||
|
<div className="card-body p-5">
|
||||||
|
<div className="text-center mb-4">
|
||||||
|
<p className="text-muted mb-1">Create your account</p>
|
||||||
|
<h1 className="fw-bold fs-2">Sign up</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
{/* Name */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control form-control-lg py-3"
|
||||||
|
placeholder="Full Name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Email */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className="form-control form-control-lg py-3"
|
||||||
|
placeholder="Email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Phone */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
className="form-control form-control-lg py-3"
|
||||||
|
placeholder="Phone Number (optional)"
|
||||||
|
value={phone}
|
||||||
|
onChange={(e) => setPhone(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Password */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-control form-control-lg py-3"
|
||||||
|
placeholder="Password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
minLength={6}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Confirm Password */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="form-control form-control-lg py-3"
|
||||||
|
placeholder="Confirm Password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<div className="d-grid mb-4">
|
||||||
|
<button type="submit" className="btn btn-primary btn-lg py-3 fw-bold">
|
||||||
|
Sign Up
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Login Link */}
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="mb-0 small text-muted">
|
||||||
|
Already have an account?{' '}
|
||||||
|
<Link to="/login" className="text-decoration-none fw-medium text-danger">
|
||||||
|
Log in
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
frontend/src/components/VerifyEmail.jsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import Navbar from './Navbar';
|
||||||
|
|
||||||
|
export default function VerifyEmail() {
|
||||||
|
const email = "example@eg.com";
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<>
|
||||||
|
|
||||||
|
<Navbar />
|
||||||
|
<div className="vh-100 bg-light d-flex flex-column">
|
||||||
|
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="flex-grow-1 d-flex align-items-center justify-content-center p-4" style={{ marginTop: "-100px" }}>
|
||||||
|
{/* <div className="w-100" style={{ maxWidth: '500px' }}> */}
|
||||||
|
<div className="card border-0 shadow-lg rounded-4">
|
||||||
|
<div className="card-body p-5">
|
||||||
|
<div className="text-center mb-4">
|
||||||
|
<h1 className="fw-bold fs-2">Verify your E-mail address</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-muted mb-3">
|
||||||
|
We’ve sent a confirmation email to <strong>{email}</strong>. Please click the link in the email to continue.
|
||||||
|
</p>
|
||||||
|
<p className="mb-0">
|
||||||
|
Didn’t get the email?{' '}
|
||||||
|
<a href="#" className="text-decoration-none text-primary fw-medium">
|
||||||
|
Resend Email
|
||||||
|
</a>.
|
||||||
|
{' '}Entered the wrong address?{' '}
|
||||||
|
<a href="#" className="text-decoration-none text-primary fw-medium">
|
||||||
|
Change Email
|
||||||
|
</a>.
|
||||||
|
{' '}Need help?{' '}
|
||||||
|
<a href="#" className="text-decoration-none text-primary fw-medium">
|
||||||
|
Contact Support
|
||||||
|
</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Success Alert at Bottom */}
|
||||||
|
<div
|
||||||
|
className="alert alert-success d-flex align-items-center justify-content-between mt-4"
|
||||||
|
role="alert"
|
||||||
|
style={{
|
||||||
|
borderRadius: '1rem',
|
||||||
|
padding: '0.75rem 1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>✅ Confirmation email sent to {email}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-close"
|
||||||
|
aria-label="Close"
|
||||||
|
style={{ fontSize: '0.75rem' }}
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* </div> */}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
frontend/src/index.css
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
21
frontend/src/index.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import './index.css';
|
||||||
|
import App from './App';
|
||||||
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
|
||||||
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
|
reportWebVitals();
|
||||||
1
frontend/src/logo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
132
frontend/src/pages/Dashboard.jsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Navbar from '../components/Navbar';
|
||||||
|
const Dashboard = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Navbar />
|
||||||
|
<div className="container-fluid p-4" style={{ backgroundColor: '#f8f9fa' }}>
|
||||||
|
|
||||||
|
<div className="card border-0 shadow-sm mb-4 mx-auto mt-4" style={{ width: '85%', borderRadius: '12px' , height: '300px' }}>
|
||||||
|
<div className="card-body p-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control mb-3"
|
||||||
|
placeholder="Write your research query here..."
|
||||||
|
style={{ borderColor: '#0d6efd', borderWidth: '2px', padding: '1rem' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="d-flex flex-wrap align-items-center gap-3">
|
||||||
|
<div className="dropdown">
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-primary dropdown-toggle d-flex align-items-center gap-2"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
style={{ borderColor: '#0d6efd', borderWidth: '2px' }}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-paperclip" viewBox="0 0 16 16">
|
||||||
|
<path d="M4.5 3a2.5 2.5 0 0 1 5 0v9a1.5 1.5 0 0 1-3 0V5.5a1.5 1.5 0 0 1-3 0zm5 0v10.5a1.5 1.5 0 0 1-3 0V5.5a1.5 1.5 0 0 1-3 0V3z"/>
|
||||||
|
</svg>
|
||||||
|
Attachments
|
||||||
|
</button>
|
||||||
|
<ul className="dropdown-menu">
|
||||||
|
<li><a className="dropdown-item" href="#">File 1</a></li>
|
||||||
|
<li><a className="dropdown-item" href="#">File 2</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="dropdown">
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-primary dropdown-toggle d-flex align-items-center gap-2"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
style={{ borderColor: '#0d6efd', borderWidth: '2px' }}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-book" viewBox="0 0 16 16">
|
||||||
|
<path d="M1 2.828c.764-.763 2.077-.763 2.84 0l6.421 6.421a.75.75 0 0 0 .577.217.75.75 0 0 0 .577-.217L15.67 2.828A6.964 6.964 0 0 0 12 1.5a6.964 6.964 0 0 0-4.878 2.103L5 5.5a6.964 6.964 0 0 0-4.878 2.103z"/>
|
||||||
|
</svg>
|
||||||
|
Choose discipline
|
||||||
|
</button>
|
||||||
|
<ul className="dropdown-menu">
|
||||||
|
<li><a className="dropdown-item" href="#">Computer Science</a></li>
|
||||||
|
<li><a className="dropdown-item" href="#">Medicine</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="dropdown">
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-primary dropdown-toggle d-flex align-items-center gap-2"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
style={{ borderColor: '#0d6efd', borderWidth: '2px' }}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-file-earmark-text" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zM5 9.5a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1z"/>
|
||||||
|
</svg>
|
||||||
|
Templates
|
||||||
|
</button>
|
||||||
|
<ul className="dropdown-menu">
|
||||||
|
<li><a className="dropdown-item" href="#">Template 1</a></li>
|
||||||
|
<li><a className="dropdown-item" href="#">Template 2</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Create Button (Blue, not orange) */}
|
||||||
|
<button
|
||||||
|
className="btn btn-primary ms-auto"
|
||||||
|
style={{ borderRadius: '20px', padding: '0.5rem 1.5rem' }}
|
||||||
|
>
|
||||||
|
CREATE
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Projects Section */}
|
||||||
|
<h5 className="mb-3 fw-bold">Projects</h5>
|
||||||
|
<div className="row g-4">
|
||||||
|
{/* Project Card 1 */}
|
||||||
|
<div className="col-md-4">
|
||||||
|
<div className="card border-0 shadow-sm h-100" style={{ borderRadius: '12px' }}>
|
||||||
|
<div className="card-body">
|
||||||
|
<h6 className="card-title fw-bold mb-2">AI-Powered Summarization for Academic Research</h6>
|
||||||
|
<p className="card-text text-muted">
|
||||||
|
This project proposes the design and development of an AI-driven text summarization tool to assist...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Project Card 2 */}
|
||||||
|
<div className="col-md-4">
|
||||||
|
<div className="card border-0 shadow-sm h-100" style={{ borderRadius: '12px' }}>
|
||||||
|
<div className="card-body">
|
||||||
|
<h6 className="card-title fw-bold mb-2">AI-Powered Summarization for Academic Research</h6>
|
||||||
|
<p className="card-text text-muted">
|
||||||
|
This project proposes the design and development of an AI-driven text summarization tool to assist...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Project Card 3 */}
|
||||||
|
<div className="col-md-4">
|
||||||
|
<div className="card border-0 shadow-sm h-100" style={{ borderRadius: '12px' }}>
|
||||||
|
<div className="card-body">
|
||||||
|
<h6 className="card-title fw-bold mb-2">AI-Powered Summarization for Academic Research</h6>
|
||||||
|
<p className="card-text text-muted">
|
||||||
|
This project proposes the design and development of an AI-driven text summarization tool to assist...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
98
frontend/src/pages/login.html
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
|
||||||
|
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="Yuri admin is super flexible, powerful, clean & modern responsive bootstrap 5 admin template with unlimited possibilities.">
|
||||||
|
<meta name="keywords" content="admin template, Yuri admin template, dashboard template, flat admin template, responsive admin template, web app">
|
||||||
|
<meta name="author" content="pixelstrap">
|
||||||
|
<link rel="icon" href="../assets/images/favicon.png" type="image/x-icon">
|
||||||
|
<link rel="shortcut icon" href="../assets/images/favicon.png" type="image/x-icon">
|
||||||
|
<title>Yuri - Premium Admin Template</title>
|
||||||
|
<!-- Google font-->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&family=Nunito+Sans:ital,wght@0,300;0,400;0,700;0,800;0,900;1,700&display=swap" rel="stylesheet">
|
||||||
|
<!-- Font Awesome-->
|
||||||
|
<link rel="stylesheet" type="text/css" href="../assets/css/font-awesome.css">
|
||||||
|
<!-- ico-font-->
|
||||||
|
<link rel="stylesheet" type="text/css" href="../assets/css/vendors/icofont.css">
|
||||||
|
<!-- Themify icon-->
|
||||||
|
<link rel="stylesheet" type="text/css" href="../assets/css/vendors/themify.css">
|
||||||
|
<!-- Flag icon-->
|
||||||
|
<link rel="stylesheet" type="text/css" href="../assets/css/vendors/flag-icon.css">
|
||||||
|
<!-- Feather icon-->
|
||||||
|
<link rel="stylesheet" type="text/css" href="../assets/css/vendors/feather-icon.css">
|
||||||
|
<!-- Plugins css start-->
|
||||||
|
<!-- Plugins css Ends-->
|
||||||
|
<!-- Bootstrap css-->
|
||||||
|
<link rel="stylesheet" type="text/css" href="../assets/css/vendors/bootstrap.css">
|
||||||
|
<!-- App css-->
|
||||||
|
<link rel="stylesheet" type="text/css" href="../assets/css/style.css">
|
||||||
|
<!-- Responsive css-->
|
||||||
|
<link rel="stylesheet" type="text/css" href="../assets/css/responsive.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- login page start-->
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xl-7"><img class="bg-img-cover bg-center" src="../assets/images/login/2.jpg" alt="looginpage"></div>
|
||||||
|
<div class="col-xl-5 p-0">
|
||||||
|
<div class="login-card login-dark">
|
||||||
|
<div>
|
||||||
|
<div><a class="logo" href="index.html"><img class="img-fluid for-light" src="../assets/images/logo/logo.png" alt="looginpage"><img class="img-fluid for-dark" src="../assets/images/logo/logo_dark.png" alt="looginpage"></a></div>
|
||||||
|
<div class="login-main">
|
||||||
|
<form class="theme-form">
|
||||||
|
<h2>Sign in to account</h2>
|
||||||
|
<p class="f-m-light mt-1">Enter your email & password to login</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label">Email Address</label>
|
||||||
|
<input class="form-control" type="email" required="" placeholder="Test@gmail.com">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label">Password</label>
|
||||||
|
<div class="form-input position-relative">
|
||||||
|
<input class="form-control" type="password" name="login[password]" required="" placeholder="*********">
|
||||||
|
<div class="show-hide"><span class="show"> </span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group mb-0">
|
||||||
|
<div class="checkbox p-0">
|
||||||
|
<input id="checkbox1" type="checkbox">
|
||||||
|
<label class="text-muted" for="checkbox1">Remember password</label>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary btn-block w-100" type="submit">Sign in</button>
|
||||||
|
</div>
|
||||||
|
<h6 class="text-muted mt-4 or">Or Sign in with</h6>
|
||||||
|
<div class="social mt-4">
|
||||||
|
<div class="btn-showcase"><a class="btn btn-light" href="https://www.linkedin.com/login" target="_blank"><i class="txt-linkedin" data-feather="linkedin"></i> LinkedIn </a><a class="btn btn-light" href="https://twitter.com/login?lang=en" target="_blank"><i class="txt-twitter" data-feather="twitter"></i>twitter</a><a class="btn btn-light" href="https://www.facebook.com/" target="_blank"><i class="txt-fb" data-feather="facebook"></i>facebook</a></div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 mb-0 text-center">Don't have account?<a class="ms-2" href="sign-up.html">Create Account</a></p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- latest jquery-->
|
||||||
|
<script src="../assets/js/jquery.min.js"></script>
|
||||||
|
<!-- Bootstrap js-->
|
||||||
|
<script src="../assets/js/bootstrap/bootstrap.bundle.min.js"></script>
|
||||||
|
<!-- feather icon js-->
|
||||||
|
<script src="../assets/js/icons/feather-icon/feather.min.js"></script>
|
||||||
|
<script src="../assets/js/icons/feather-icon/feather-icon.js"></script>
|
||||||
|
<!-- scrollbar js-->
|
||||||
|
<!-- Sidebar jquery-->
|
||||||
|
<script src="../assets/js/config.js"></script>
|
||||||
|
<!-- Plugins JS start-->
|
||||||
|
<!-- Plugins JS Ends-->
|
||||||
|
<!-- Theme js-->
|
||||||
|
<script src="../assets/js/script.js"></script>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
frontend/src/reportWebVitals.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const reportWebVitals = onPerfEntry => {
|
||||||
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
|
getCLS(onPerfEntry);
|
||||||
|
getFID(onPerfEntry);
|
||||||
|
getFCP(onPerfEntry);
|
||||||
|
getLCP(onPerfEntry);
|
||||||
|
getTTFB(onPerfEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reportWebVitals;
|
||||||
5
frontend/src/setupTests.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||||
|
// allows you to do things like:
|
||||||
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
|
import '@testing-library/jest-dom';
|
||||||