inital commit
29
ditch-the-agent/.eslintrc.cjs
Normal file
@@ -0,0 +1,29 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
globals: {
|
||||
process: true,
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/no-unescaped-entities': 'off',
|
||||
'prettier/prettier': ['error', { endOfLine: 'auto' }],
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'@typescript-eslint/no-empty-interface': 'off',
|
||||
'react-refresh/only-export-components': 'off',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'react-hooks/rules-of-hooks': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
},
|
||||
};
|
||||
24
ditch-the-agent/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
7
ditch-the-agent/.prettierignore
Normal file
@@ -0,0 +1,7 @@
|
||||
.next/
|
||||
.vscode-test/
|
||||
out/
|
||||
dist/
|
||||
node_modules/
|
||||
public/
|
||||
build/
|
||||
19
ditch-the-agent/.prettierrc.cjs
Normal file
@@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
overrides: [
|
||||
{
|
||||
files: ['docs/**/*.md', 'docs/src/pages/**/*.{js,tsx}', 'docs/data/**/*.{js,tsx}'],
|
||||
options: {
|
||||
printWidth: 85,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['docs/pages/blog/**/*.md'],
|
||||
options: {
|
||||
printWidth: 82,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
139
ditch-the-agent/README.md
Normal file
@@ -0,0 +1,139 @@
|
||||
<a name="readme-top"></a>
|
||||
|
||||
<!-- PROJECT LOGO -->
|
||||
<br />
|
||||
<!-- PROJECT LOGO -->
|
||||
<div align="left" >
|
||||
<center>
|
||||
<a href="public/elegent-logo.png" align="center">
|
||||
<img src="public/elegent-logo.png" alt="Logo" width="50" height="50">
|
||||
</a>
|
||||
</center>
|
||||
<center>
|
||||
<h1 style="display: inline-block; margin-left: 10px;">Elegent Admin Dashboard</h1>
|
||||
</center>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<br />
|
||||
<br />
|
||||
<!-- TABLE OF CONTENTS -->
|
||||
<details align="left">
|
||||
<summary>Table of Contents</summary>
|
||||
<ol>
|
||||
<li>
|
||||
<a href="#about-the-project">About The Project</a>
|
||||
<ul>
|
||||
<li><a href="#built-with">Built With</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#getting-started">Getting Started</a>
|
||||
<ul>
|
||||
<li><a href="#prerequisites">Prerequisites</a></li>
|
||||
<li><a href="#installation">Installation</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#license">License</a></li>
|
||||
<li><a href="#license">Acknowledgments</a></li>
|
||||
</ol>
|
||||
</details>
|
||||
<br />
|
||||
<br />
|
||||
<!-- ABOUT THE PROJECT -->
|
||||
|
||||
## About The Project
|
||||
|
||||
[![Product Name Screen Shot][product-screenshot]](public/homepage.png)
|
||||
|
||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||
|
||||
### <h3>Built With :</h3>
|
||||
|
||||
[![React][React.js]][React-url]
|
||||
[![Material][Material]][React-url]
|
||||
![E-Chart][Apache-chart]
|
||||
|
||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||
|
||||
<!-- GETTING STARTED -->
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before you begin, ensure you have met the following prerequisites:
|
||||
|
||||
- [Node.js](https://nodejs.org/) installed on your local machine
|
||||
- npm or yarn package manager installed with Node.js
|
||||
|
||||
### Installation
|
||||
|
||||
Follow these steps to get your project up and running:
|
||||
|
||||
1. **Clone the repository**
|
||||
```sh
|
||||
git clone https://github.com/themewagon/elegent.git
|
||||
```
|
||||
2. **Navigate to the project directory**
|
||||
```sh
|
||||
cd elegent
|
||||
```
|
||||
3. **Install dependencies**
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
4. **Start the development server**
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
Open your web browser and navigate to http://localhost:3000/elegent to view this application.
|
||||
|
||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||
|
||||
<!-- LICENSE -->
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the MIT License. See `LICENSE.txt` for more information.
|
||||
|
||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||
|
||||
<a name="readme-top">
|
||||
<div align="">
|
||||
<a align="center" href="https://github.com/themewagon/elegent/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=themewagon/elegent" /><br />
|
||||
</a></a></div>
|
||||
|
||||
<!-- MARKDOWN LINKS & IMAGES -->
|
||||
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
|
||||
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/othneildrew/Best-README-Template.svg?style=for-the-badge
|
||||
[contributors-url]: https://github.com/othneildrew/Best-README-Template/graphs/contributors
|
||||
[forks-shield]: https://img.shields.io/github/forks/othneildrew/Best-README-Template.svg?style=for-the-badge
|
||||
[forks-url]: https://github.com/othneildrew/Best-README-Template/network/members
|
||||
[stars-shield]: https://img.shields.io/github/stars/othneildrew/Best-README-Template.svg?style=for-the-badge
|
||||
[stars-url]: https://github.com/othneildrew/Best-README-Template/stargazers
|
||||
[issues-shield]: https://img.shields.io/github/issues/othneildrew/Best-README-Template.svg?style=for-the-badge
|
||||
[issues-url]: https://github.com/othneildrew/Best-README-Template/issues
|
||||
[license-shield]: https://img.shields.io/github/license/othneildrew/Best-README-Template.svg?style=for-the-badge
|
||||
[license-url]: https://github.com/othneildrew/Best-README-Template/blob/master/LICENSE.txt
|
||||
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555
|
||||
[linkedin-url]: https://linkedin.com/in/othneildrew
|
||||
[product-screenshot]: public/homepage.png
|
||||
[Next.js]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white
|
||||
[Next-url]: https://nextjs.org/
|
||||
[React.js]: https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB
|
||||
[React-url]: https://reactjs.org/
|
||||
[Vue.js]: https://img.shields.io/badge/Vue.js-35495E?style=for-the-badge&logo=vuedotjs&logoColor=4FC08D
|
||||
[Vue-url]: https://vuejs.org/
|
||||
[Angular.io]: https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular&logoColor=white
|
||||
[Angular-url]: https://angular.io/
|
||||
[Svelte.dev]: https://img.shields.io/badge/Svelte-4A4A55?style=for-the-badge&logo=svelte&logoColor=FF3E00
|
||||
[Svelte-url]: https://svelte.dev/
|
||||
[Laravel.com]: https://img.shields.io/badge/Laravel-FF2D20?style=for-the-badge&logo=laravel&logoColor=white
|
||||
[Laravel-url]: https://laravel.com
|
||||
[Bootstrap.com]: https://img.shields.io/badge/Bootstrap-563D7C?style=for-the-badge&logo=bootstrap&logoColor=white
|
||||
[Bootstrap-url]: https://getbootstrap.com
|
||||
[Material]: https://img.shields.io/badge/Material%20UI-007FFF?style=for-the-badge&logo=mui&logoColor=white
|
||||
[Apache-chart]: https://img.shields.io/badge/echart-4.7.0-green
|
||||
13
ditch-the-agent/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon-logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Ditch The Agent</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
5415
ditch-the-agent/package-lock.json
generated
Normal file
48
ditch-the-agent/package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "mui-dta-dashboard",
|
||||
"private": true,
|
||||
"version": "1.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"predeploy": "vite build && cp ./dist/index.html ./dist/404.html",
|
||||
"deploy": "gh-pages -d dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@mui/material": "^5.15.14",
|
||||
"@mui/x-data-grid": "^7.2.0",
|
||||
"@mui/x-data-grid-generator": "^7.2.0",
|
||||
"axios": "^1.10.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"echarts": "^5.5.0",
|
||||
"echarts-for-react": "^3.0.2",
|
||||
"formik": "^2.4.6",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lucide-react": "^0.525.0",
|
||||
"material-ui-popup-state": "^5.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"simplebar-react": "^3.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/react": "^4.1.1",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||
"@typescript-eslint/parser": "^7.2.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.2.0",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
}
|
||||
}
|
||||
BIN
ditch-the-agent/public/elegent-favicon-logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
ditch-the-agent/public/favicon-logo.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
ditch-the-agent/public/homepage.png
Normal file
|
After Width: | Height: | Size: 287 KiB |
1
ditch-the-agent/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
5
ditch-the-agent/src/App.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
const App = () => <Outlet />;
|
||||
|
||||
export default App;
|
||||
BIN
ditch-the-agent/src/assets/404/404.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 29 KiB |
BIN
ditch-the-agent/src/assets/authentication-banners/green.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
ditch-the-agent/src/assets/authentication-banners/login.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 29 KiB |
BIN
ditch-the-agent/src/assets/authentication-banners/signup.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
BIN
ditch-the-agent/src/assets/logo/elegant-logo.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
ditch-the-agent/src/assets/logo/elegent-favicon-logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
ditch-the-agent/src/assets/logo/favicon-logo.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
ditch-the-agent/src/assets/new-customers/darron.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
ditch-the-agent/src/assets/new-customers/jone.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
ditch-the-agent/src/assets/new-customers/leatrice.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
ditch-the-agent/src/assets/new-customers/mail-icon.png
Normal file
|
After Width: | Height: | Size: 997 B |
BIN
ditch-the-agent/src/assets/new-customers/roselle.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
ditch-the-agent/src/assets/profile/profile.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
ditch-the-agent/src/assets/projects-overview/Gothic.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
ditch-the-agent/src/assets/projects-overview/Minimalist.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
ditch-the-agent/src/assets/projects-overview/Modern.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
ditch-the-agent/src/assets/projects-overview/Scandinavian.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
ditch-the-agent/src/assets/sale-info/avg-revenue.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
ditch-the-agent/src/assets/sale-info/bookings.png
Normal file
|
After Width: | Height: | Size: 866 B |
BIN
ditch-the-agent/src/assets/sale-info/customers.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
ditch-the-agent/src/assets/sale-info/followers.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
ditch-the-agent/src/assets/sale-info/revenue.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
ditch-the-agent/src/assets/sale-info/sales.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
ditch-the-agent/src/assets/sale-info/today-users.png
Normal file
|
After Width: | Height: | Size: 735 B |
BIN
ditch-the-agent/src/assets/top-selling-products/instaxCamera.jpg
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
ditch-the-agent/src/assets/top-selling-products/iphone12.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
ditch-the-agent/src/assets/top-selling-products/laptop.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
ditch-the-agent/src/assets/top-selling-products/nikeV22.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 49 KiB |
BIN
ditch-the-agent/src/assets/top-selling-products/watch.jpg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
ditch-the-agent/src/assets/top-selling-products/xerryPerfume.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
96
ditch-the-agent/src/axiosApi.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import axios from "axios"
|
||||
import Cookies from 'js-cookie'
|
||||
|
||||
|
||||
const baseURL = 'http://127.0.0.1:8010/api/';
|
||||
//const baseURL = 'https://backend.ditchtheagent.com/api/';
|
||||
|
||||
|
||||
export const axiosInstance = axios.create({
|
||||
baseURL: baseURL,
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
"Authorization": 'JWT ' + localStorage.getItem('access_token'),
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
export const cleanAxiosInstance = axios.create({
|
||||
baseURL: baseURL,
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
export const axiosInstanceCSRF = axios.create({
|
||||
baseURL: baseURL,
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
'X-CSRFToken': Cookies.get('csrftoken'), // Include CSRF token in headers
|
||||
},
|
||||
withCredentials: true,
|
||||
}
|
||||
);
|
||||
|
||||
axiosInstance.interceptors.request.use(config => {
|
||||
config.timeout = 100000;
|
||||
return config;
|
||||
})
|
||||
|
||||
axiosInstance.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
// Prevent infinite loop
|
||||
if (error.response.status === 401 && originalRequest.url === baseURL+'/token/refresh/') {
|
||||
window.location.href = '/signin/';
|
||||
//console.log('remove the local storage here')
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if(error.response.data.code === "token_not_valid" &&
|
||||
error.response.status == 401 &&
|
||||
error.response.statusText == 'Unauthorized')
|
||||
{
|
||||
const refresh_token = localStorage.getItem('refresh_token');
|
||||
|
||||
if (refresh_token){
|
||||
const tokenParts = JSON.parse(atob(refresh_token.split('.')[1]));
|
||||
|
||||
const now = Math.ceil(Date.now() / 1000);
|
||||
//console.log(tokenParts.exp)
|
||||
|
||||
if(tokenParts.exp > now){
|
||||
return axiosInstance.post('/token/refresh/', {refresh: refresh_token}).then((response) => {
|
||||
localStorage.setItem('access_token', response.data.access);
|
||||
localStorage.setItem('refresh_token', response.data.refresh);
|
||||
|
||||
axiosInstance.defaults.headers['Authorization'] = 'JWT ' + response.data.access;
|
||||
originalRequest.headers['Authorization'] = 'JWT ' + response.data.access;
|
||||
|
||||
return axiosInstance(originalRequest);
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
});
|
||||
|
||||
}else{
|
||||
console.log('Refresh token is expired');
|
||||
window.location.href = '/signin/';
|
||||
|
||||
}
|
||||
}else {
|
||||
console.log('Refresh token not available');
|
||||
window.location.href = '/signin/';
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
371
ditch-the-agent/src/components/FloatingChatButton.tsx
Normal file
@@ -0,0 +1,371 @@
|
||||
import { ReactElement, useState, useEffect, useRef, ChangeEvent, KeyboardEvent } from 'react';
|
||||
import { MessageSquareText, Minus, X, Send } from 'lucide-react'; // Using lucide-react for icons
|
||||
import { Box, Button, AppBar, Typography, useTheme, Fab, TextField, Paper, Toolbar, IconButton } from '@mui/material';
|
||||
|
||||
|
||||
interface FloatingActionButtonProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const FloatingActionButton = ({ onClick }: FloatingActionButtonProps) => {
|
||||
return (
|
||||
|
||||
<Fab
|
||||
color="secondary"
|
||||
aria-label="open chat"
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
bottom: 24, // Equivalent to Tailwind's bottom-6 (24px)
|
||||
right: 24, // Equivalent to Tailwind's right-6 (24px)
|
||||
zIndex: 50, // Equivalent to Tailwind's z-50
|
||||
boxShadow: '0px 10px 15px -3px rgba(0,0,0,0.1), 0px 4px 6px -2px rgba(0,0,0,0.05)', // Tailwind shadow-lg
|
||||
'&:hover': {
|
||||
backgroundColor: '#1d4ed8', // Blue-700 equivalent
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MessageSquareText size={24} />
|
||||
</Fab>
|
||||
);
|
||||
};
|
||||
|
||||
interface ChatMessage {
|
||||
text: string;
|
||||
sender: 'user' | 'ai';
|
||||
}
|
||||
|
||||
interface ChatPaneProps {
|
||||
showChat: boolean;
|
||||
isMinimized: boolean;
|
||||
toggleMinimize: () => void;
|
||||
closeChat: () => void;
|
||||
}
|
||||
|
||||
|
||||
// Chat Pane Component
|
||||
const ChatPane = ({ showChat, isMinimized, toggleMinimize, closeChat }: ChatPaneProps) => {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
// State for the current message being typed by the user
|
||||
const [inputMessage, setInputMessage] = useState<string>('');
|
||||
// State to indicate if an AI response is being loaded
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
// Ref for the messages container to scroll to the bottom
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const theme = useTheme();
|
||||
|
||||
// Scroll to the bottom of the chat window whenever messages change
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [messages]);
|
||||
|
||||
// Function to send a message to the AI
|
||||
const handleSendMessage = async () => {
|
||||
if (inputMessage.trim() === '') return;
|
||||
|
||||
const newUserMessage: ChatMessage = { text: inputMessage, sender: 'user' };
|
||||
setMessages((prevMessages: ChatMessage[]) => [...prevMessages, newUserMessage]);
|
||||
setInputMessage(''); // Clear input field
|
||||
|
||||
setIsLoading(true); // Show loading indicator
|
||||
|
||||
try {
|
||||
// Construct chat history for the API call
|
||||
let chatHistory = messages.map(msg => ({
|
||||
role: msg.sender === 'user' ? 'user' : 'model',
|
||||
parts: [{ text: msg.text }]
|
||||
}));
|
||||
chatHistory.push({ role: "user", parts: [{ text: newUserMessage.text }] });
|
||||
|
||||
const payload = { contents: chatHistory };
|
||||
const apiKey = ""; // Leave this as-is; Canvas will provide the API key at runtime
|
||||
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`;
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.candidates && result.candidates.length > 0 &&
|
||||
result.candidates[0].content && result.candidates[0].content.parts &&
|
||||
result.candidates[0].content.parts.length > 0) {
|
||||
const aiResponseText = result.candidates[0].content.parts[0].text;
|
||||
setMessages((prevMessages) => [...prevMessages, { text: aiResponseText, sender: 'ai' }]);
|
||||
} else {
|
||||
// Handle cases where the response structure is unexpected or content is missing
|
||||
console.error("Unexpected API response structure:", result);
|
||||
setMessages((prevMessages) => [...prevMessages, { text: "Error: Could not get a response from AI.", sender: 'ai' }]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching AI response:', error);
|
||||
setMessages((prevMessages) => [...prevMessages, { text: "Error: Failed to connect to AI.", sender: 'ai' }]);
|
||||
} finally {
|
||||
setIsLoading(false); // Hide loading indicator
|
||||
}
|
||||
};
|
||||
|
||||
if (!showChat) return null; // Don't render anything if chat is not shown
|
||||
|
||||
return (
|
||||
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
bottom: 24,
|
||||
right: 24,
|
||||
backgroundColor: 'white.200',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0px 20px 25px -5px rgba(0,0,0,0.1), 0px 10px 10px -5px rgba(0,0,0,0.04)', // Tailwind shadow-xl
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
transition: 'all 300ms ease-in-out',
|
||||
overflow: 'hidden',
|
||||
zIndex: 40,
|
||||
width: isMinimized ? '320px' : '384px', // w-80 (320px) vs w-96 (384px)
|
||||
height: isMinimized ? '64px' : '485px', // h-16 (64px) vs h-[600px]
|
||||
'@media (min-width: 768px)': { // md: breakpoint
|
||||
height: isMinimized ? '64px' : 'calc(100vh - 130px)', // md:h-[calc(100vh-80px)]
|
||||
},
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.200',
|
||||
}}
|
||||
>
|
||||
{/* Chat Header */}
|
||||
<AppBar position="static" color='inherit' sx={{
|
||||
backgroundColor: 'background.paper'
|
||||
}}>
|
||||
|
||||
|
||||
<Toolbar variant="dense" sx={{ justifyContent: 'space-between', minHeight: '64px' }}> {/* minHeight to match h-16 for minimized */}
|
||||
<Typography variant="h6" component="div">
|
||||
AI Assistant
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
onClick={toggleMinimize}
|
||||
aria-label={isMinimized ? "Maximize chat" : "Minimize chat"}
|
||||
>
|
||||
<Minus size={20} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
onClick={closeChat}
|
||||
aria-label="Close chat"
|
||||
>
|
||||
<X size={20} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
{/* Chat Body (visible only when not minimized) */}
|
||||
{!isMinimized && (
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
p: 2,
|
||||
overflowY: 'auto',
|
||||
backgroundColor: 'background.paper',
|
||||
}}
|
||||
className="custom-scrollbar" // Keep custom scrollbar for now
|
||||
>
|
||||
{messages.length === 0 && !isLoading && (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', mt: 5 }}>
|
||||
Start a conversation with the AI assistant!
|
||||
</Typography>
|
||||
)}
|
||||
{messages.map((msg: ChatMessage, index: number) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
mb: 2, // mb-4
|
||||
justifyContent: msg.sender === 'user' ? 'flex-end' : 'flex-start',
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={1} // shadow-sm
|
||||
sx={{
|
||||
maxWidth: '70%',
|
||||
p: 1.5, // p-3
|
||||
borderRadius: '12px',
|
||||
backgroundColor: msg.sender === 'user' ? 'primary.main' : 'grey.200',
|
||||
color: msg.sender === 'user' ? 'white' : 'text.primary',
|
||||
borderBottomRightRadius: msg.sender === 'user' ? 0 : '12px',
|
||||
borderBottomLeftRadius: msg.sender === 'user' ? '12px' : 0,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2">{msg.text}</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
))}
|
||||
{isLoading && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-start', mb: 2 }}>
|
||||
<Paper
|
||||
elevation={1}
|
||||
sx={{
|
||||
maxWidth: '70%',
|
||||
p: 1.5,
|
||||
borderRadius: '12px',
|
||||
backgroundColor: 'grey.200',
|
||||
color: 'text.primary',
|
||||
borderBottomLeftRadius: 0,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Typography component="span" className="animate-bounce" sx={{ mr: 0.5 }}>.</Typography>
|
||||
<Typography component="span" className="animate-bounce delay-100" sx={{ mr: 0.5 }}>.</Typography>
|
||||
<Typography component="span" className="animate-bounce delay-200">.</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
)}
|
||||
<div ref={messagesEndRef} /> {/* Scroll target */}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Chat Input (visible only when not minimized) */}
|
||||
{!isMinimized && (
|
||||
<Box
|
||||
sx={{
|
||||
flexShrink: 0,
|
||||
p: 2,
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'grey.200',
|
||||
backgroundColor: 'white',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1, // space-x-2
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={inputMessage}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setInputMessage(e.target.value)}
|
||||
onKeyPress={(e: KeyboardEvent<HTMLInputElement>) => e.key === 'Enter' && handleSendMessage()}
|
||||
placeholder="Type your message..."
|
||||
disabled={isLoading}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: '8px', // rounded-lg
|
||||
},
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'grey.300', // border-gray-300
|
||||
},
|
||||
'& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'primary.main', // focus:ring-blue-400 equivalent
|
||||
borderWidth: '2px',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleSendMessage}
|
||||
endIcon={<Send size={20} />}
|
||||
disabled={isLoading}
|
||||
sx={{
|
||||
p: 1.5, // p-3
|
||||
borderRadius: '8px', // rounded-lg
|
||||
boxShadow: 'none', // Remove default button shadow
|
||||
'&:hover': {
|
||||
backgroundColor: '#1d4ed8', // Blue-700 equivalent
|
||||
boxShadow: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Text is hidden on small screens, icon only */}
|
||||
<Typography sx={{ display: { xs: 'none', sm: 'block' } }}>Send</Typography>
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Tailwind CSS Custom Scrollbar and Animation */}
|
||||
<style>
|
||||
{`
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-5px); }
|
||||
}
|
||||
|
||||
.animate-bounce {
|
||||
animation: bounce 0.6s infinite alternate;
|
||||
}
|
||||
.animate-bounce.delay-100 { animation-delay: 0.1s; }
|
||||
.animate-bounce.delay-200 { animation-delay: 0.2s; }
|
||||
`}
|
||||
</style>
|
||||
</Box>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
const FloatingChatButton = (): ReactElement =>
|
||||
{
|
||||
const [showChat, setShowChat] = useState<boolean>(false);
|
||||
// State to control if the chat pane is minimized
|
||||
const [isMinimized, setIsMinimized] = useState<boolean>(false);
|
||||
|
||||
// Function to toggle the chat pane visibility
|
||||
const toggleChat = () => {
|
||||
setShowChat(!showChat);
|
||||
// When opening, ensure it's not minimized
|
||||
if (!showChat) {
|
||||
setIsMinimized(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to toggle minimize/maximize the chat pane
|
||||
const toggleMinimize = () => {
|
||||
setIsMinimized(!isMinimized);
|
||||
};
|
||||
|
||||
// Function to close the chat pane
|
||||
const closeChat = () => {
|
||||
setShowChat(false);
|
||||
setIsMinimized(false); // Reset minimize state when closing
|
||||
};
|
||||
return(
|
||||
<div className="relative h-screen w-full font-sans bg-gray-100 flex items-center justify-center">
|
||||
{/* Floating Action Button */}
|
||||
{!showChat && <FloatingActionButton onClick={toggleChat} />}
|
||||
|
||||
<ChatPane
|
||||
showChat={showChat}
|
||||
isMinimized={isMinimized}
|
||||
toggleMinimize={toggleMinimize}
|
||||
closeChat={closeChat}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default FloatingChatButton;
|
||||
12
ditch-the-agent/src/components/base/IconifyIcon.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Box, BoxProps } from '@mui/material';
|
||||
import { Icon, IconProps } from '@iconify/react';
|
||||
|
||||
interface IconifyProps extends BoxProps {
|
||||
icon: IconProps['icon'];
|
||||
}
|
||||
|
||||
const IconifyIcon = ({ icon, width, height, ...rest }: IconifyProps) => {
|
||||
return <Box component={Icon} icon={icon} {...rest} width={width} height={height}></Box>;
|
||||
};
|
||||
|
||||
export default IconifyIcon;
|
||||
14
ditch-the-agent/src/components/base/Image.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Box, SxProps } from '@mui/material';
|
||||
import { ImgHTMLAttributes } from 'react';
|
||||
|
||||
interface ImageProps extends ImgHTMLAttributes<HTMLImageElement> {
|
||||
src: string;
|
||||
alt?: string;
|
||||
sx?: SxProps;
|
||||
}
|
||||
|
||||
const Image = ({ src, alt, sx, ...rest }: ImageProps) => (
|
||||
<Box component="img" src={src} alt={alt} sx={sx} {...rest} />
|
||||
);
|
||||
|
||||
export default Image;
|
||||
31
ditch-the-agent/src/components/base/ReactEchart.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Box, BoxProps } from '@mui/material';
|
||||
import { EChartsReactProps } from 'echarts-for-react';
|
||||
import EChartsReactCore from 'echarts-for-react/lib/core';
|
||||
import ReactEChartsCore from 'echarts-for-react/lib/core';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
export interface ReactEchartProps extends BoxProps {
|
||||
echarts: EChartsReactProps['echarts'];
|
||||
option: EChartsReactProps['option'];
|
||||
}
|
||||
|
||||
const ReactEchart = forwardRef<null | EChartsReactCore, ReactEchartProps>(
|
||||
({ option, ...rest }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
component={ReactEChartsCore}
|
||||
ref={ref}
|
||||
option={{
|
||||
...option,
|
||||
tooltip: {
|
||||
...option.tooltip,
|
||||
confine: true,
|
||||
},
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default ReactEchart;
|
||||
28
ditch-the-agent/src/components/loading/PageLoader.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Box, CircularProgress, Stack, StackOwnProps } from '@mui/material';
|
||||
import { caribbeanGreen, downy, orange, watermelon } from 'theme/colors';
|
||||
|
||||
const PageLoader = (props: StackOwnProps) => {
|
||||
return (
|
||||
<Stack alignItems="center" width={1} justifyContent="center" height={1} {...props}>
|
||||
<Box height={'10vh'} width={'25vw'} textAlign={'center'}>
|
||||
<svg width={0} height={0}>
|
||||
<defs>
|
||||
<linearGradient id="my_gradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stopColor={orange[500]} />
|
||||
<stop offset="33%" stopColor={caribbeanGreen[500]} />
|
||||
<stop offset="67%" stopColor={downy[500]} />
|
||||
<stop offset="100%" stopColor={watermelon[500]} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<CircularProgress
|
||||
size={100}
|
||||
thickness={3}
|
||||
sx={{ 'svg circle': { stroke: 'url(#my_gradient)' } }}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageLoader;
|
||||
11
ditch-the-agent/src/components/loading/Splash.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Box, LinearProgress } from '@mui/material';
|
||||
|
||||
const Splash = () => {
|
||||
return (
|
||||
<Box sx={{ width: 1, height: '100vh' }}>
|
||||
<LinearProgress color="primary" />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Splash;
|
||||
@@ -0,0 +1,65 @@
|
||||
|
||||
import { ReactElement } from 'react';
|
||||
import { Card, CardContent, CardMedia, Divider, Stack, Typography } from '@mui/material';
|
||||
import { DataGrid } from '@mui/x-data-grid';
|
||||
|
||||
type EducationDetailProps = {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const EducationDetail = (): ReactElement => {
|
||||
return(
|
||||
<Card
|
||||
sx={(theme) => ({
|
||||
boxShadow: theme.shadows[4],
|
||||
width: 1,
|
||||
height: 'auto',
|
||||
})}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
flex: '1 1 auto',
|
||||
padding: 0,
|
||||
':last-child': {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" flexWrap="wrap">
|
||||
<Typography variant="subtitle1" component="p" minWidth={100} color="text.primary">
|
||||
{'Video'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
|
||||
<Stack
|
||||
bgcolor="background.paper"
|
||||
borderRadius={5}
|
||||
width={1}
|
||||
boxShadow={(theme) => theme.shadows[4]}
|
||||
height={1}
|
||||
>
|
||||
<CardMedia
|
||||
component='video'
|
||||
className={''}
|
||||
image={"https://www.youtube.com/watch?v=2yJgwwDcgV8&list=RD2yJgwwDcgV8&start_radio=1"}
|
||||
autoPlay
|
||||
/>
|
||||
|
||||
|
||||
|
||||
</Stack>
|
||||
|
||||
|
||||
|
||||
</CardContent>
|
||||
|
||||
|
||||
</Card>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default EducationDetail;
|
||||
@@ -0,0 +1,214 @@
|
||||
|
||||
import { ReactElement } from 'react';
|
||||
import { Box, Card, CardContent, CardMedia, Divider, LinearProgress, Stack, Typography } from '@mui/material';
|
||||
import { DataGrid, GridRenderCellParams } from '@mui/x-data-grid';
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { renderProgress } from '@mui/x-data-grid-generator';
|
||||
import { GridColDef } from '@mui/x-data-grid';
|
||||
|
||||
type EducationInfoProps = {
|
||||
title: string;
|
||||
}
|
||||
interface Row {
|
||||
id: number;
|
||||
task: string;
|
||||
progress: number; // Value from 0 to 100 for the progress bar
|
||||
}
|
||||
export const EducationInfoCards = () => {
|
||||
return(
|
||||
<Stack direction={{sm:'row'}} justifyContent={{ sm: 'space-between' }} gap={3.75}>
|
||||
<EducationInfo title={'Education'} />
|
||||
|
||||
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const EducationInfo = ({ title }: EducationInfoProps): ReactElement => {
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'id',
|
||||
headerName: 'ID'
|
||||
},
|
||||
{
|
||||
field: 'title',
|
||||
headerName: 'Title',
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
{
|
||||
field: 'category',
|
||||
headerName: 'Category',
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
{
|
||||
field: 'progress',
|
||||
headerName: 'Progress',
|
||||
flex: 1,
|
||||
renderCell: (params: GridRenderCellParams<Row, number>) => {
|
||||
const progressValue = params.value; // Access the progress value from the row data
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center' }}>
|
||||
<Box sx={{ width: '100%', mr: 1 }}>
|
||||
<LinearProgress variant="determinate" value={progressValue} />
|
||||
</Box>
|
||||
<Box sx={{ minWidth: 35 }}>
|
||||
<Typography variant="body2" color="text.secondary">{`${progressValue}%`}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
headerName: 'Status',
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
const rows = [
|
||||
{
|
||||
id: 1,
|
||||
title: "How to Research Comparable Properties Like a Pro",
|
||||
category: "Pricing Strategy",
|
||||
progress: 100,
|
||||
status: "COMPLETED",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Understanding Price Per Square Foot in Your Neighborhood",
|
||||
category: "Pricing Strategy",
|
||||
progress: 100,
|
||||
status: "COMPLETED",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Psychological Pricing: Why $399,900 Works Better Than $400,000",
|
||||
category: "Pricing Strategy",
|
||||
progress: 100,
|
||||
status: "COMPLETED",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "When and How to Adjust Your Asking Price",
|
||||
category: "Pricing Strategy",
|
||||
progress: 100,
|
||||
status: "COMPLETED",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "Handling Lowball Offers: Strategies That Work",
|
||||
category: "Pricing Strategy",
|
||||
progress: 100,
|
||||
status: "COMPLETED",
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{
|
||||
id: 6,
|
||||
title: "The Ultimate Home Staging Checklist for FSBO Sellers",
|
||||
category: "Property Preparation",
|
||||
progress: 90,
|
||||
status: "IN_PROGRESS",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: "DIY Curb Appeal Upgrades Under $500",
|
||||
category: "Property Preparation",
|
||||
progress: 80,
|
||||
status: "IN_PROGRESS",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: "Decluttering Secrets for Faster Sales",
|
||||
category: "Property Preparation",
|
||||
progress: 5,
|
||||
status: "IN_PROGRESS",
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
title: "Professional Photography Tips Using Just Your Smartphone",
|
||||
category: "Property Preparation",
|
||||
progress: 50,
|
||||
status: "IN_PROGRESS",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
title: "Deep Cleaning Checklist Before Listing",
|
||||
category: "Property Preparation",
|
||||
progress: 50,
|
||||
status: "IN_PROGRESS",
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
title: "How to stage a home",
|
||||
category: "",
|
||||
progress: 0,
|
||||
status: "NOT_STARTED",
|
||||
},
|
||||
]
|
||||
return(
|
||||
<Card
|
||||
sx={(theme) => ({
|
||||
boxShadow: theme.shadows[4],
|
||||
width: 1,
|
||||
height: 'auto',
|
||||
})}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
flex: '1 1 auto',
|
||||
padding: 0,
|
||||
':last-child': {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" flexWrap="wrap">
|
||||
<Typography variant="subtitle1" component="p" minWidth={100} color="text.primary">
|
||||
{title}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
|
||||
<Stack
|
||||
bgcolor="background.paper"
|
||||
borderRadius={5}
|
||||
width={1}
|
||||
boxShadow={(theme) => theme.shadows[4]}
|
||||
height={1}
|
||||
>
|
||||
<DataGrid
|
||||
getRowHeight={() => 70}
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
onRowClick={(event) => navigate('lesson')}
|
||||
/>
|
||||
|
||||
|
||||
</Stack>
|
||||
|
||||
|
||||
|
||||
</CardContent>
|
||||
|
||||
|
||||
</Card>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default EducationInfo;
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { Card, CardContent, CardMedia, Divider, Stack, Typography } from '@mui/material';
|
||||
|
||||
const HomePriceEstimate = (): ReactElement => {
|
||||
return(
|
||||
<Card
|
||||
sx={(theme) => ({
|
||||
boxShadow: theme.shadows[4],
|
||||
width: 1,
|
||||
height: 'auto',
|
||||
})}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
flex: '1 1 auto',
|
||||
padding: 0,
|
||||
':last-child': {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" flexWrap="wrap">
|
||||
<Typography variant="subtitle1" component="h3" minWidth={100} color="text.primary">
|
||||
Home Price Estimate
|
||||
</Typography>
|
||||
|
||||
</Stack>
|
||||
<Stack direction="column" justifyContent="space-between" flexWrap="wrap">
|
||||
<Typography>
|
||||
<b>$700,500k</b>
|
||||
</Typography>
|
||||
<Typography>
|
||||
Estimated value range: $335,000 - $365,000
|
||||
</Typography>
|
||||
<Typography>
|
||||
Last updated: June 15, 2023
|
||||
</Typography>
|
||||
<Divider />
|
||||
<Typography>
|
||||
This estimate is based on recent sales and market trends in your area.
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
|
||||
|
||||
</CardContent>
|
||||
|
||||
|
||||
</Card>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default HomePriceEstimate;
|
||||
@@ -0,0 +1,57 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { Card, CardContent, CardMedia, LinearProgress, Stack, Typography } from '@mui/material';
|
||||
|
||||
const LoanDetailsCard = (): ReactElement => {
|
||||
return(
|
||||
<Card
|
||||
sx={(theme) => ({
|
||||
boxShadow: theme.shadows[4],
|
||||
width: 1,
|
||||
height: 'auto',
|
||||
})}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
flex: '1 1 auto',
|
||||
padding: 0,
|
||||
':last-child': {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" flexWrap="wrap">
|
||||
<Typography variant="subtitle1" component="p" minWidth={100} color="text.primary">
|
||||
Loan Details
|
||||
</Typography>
|
||||
|
||||
</Stack>
|
||||
<Stack direction="column" justifyContent="space-between" flexWrap="wrap">
|
||||
<LinearProgress variant="determinate" value={20} />
|
||||
<Typography>
|
||||
Length: 30 year
|
||||
</Typography>
|
||||
<Typography>
|
||||
Start Date: Dec 2020
|
||||
</Typography>
|
||||
<Typography>
|
||||
Intrest Rate: 3%
|
||||
</Typography>
|
||||
<Typography>
|
||||
Intrest Rate: 3%
|
||||
</Typography>
|
||||
<Typography>
|
||||
PMI: 3%
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
|
||||
|
||||
</CardContent>
|
||||
|
||||
|
||||
</Card>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default LoanDetailsCard;
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { Card, CardContent, CardMedia, Stack, Typography } from '@mui/material';
|
||||
|
||||
const MarketStatistics = (): ReactElement => {
|
||||
return(
|
||||
<Card
|
||||
sx={(theme) => ({
|
||||
boxShadow: theme.shadows[4],
|
||||
width: 1,
|
||||
height: 'auto',
|
||||
})}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
flex: '1 1 auto',
|
||||
padding: 0,
|
||||
':last-child': {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" flexWrap="wrap">
|
||||
<Typography variant="subtitle1" component="p" minWidth={100} color="text.primary">
|
||||
Market Statistics
|
||||
</Typography>
|
||||
|
||||
</Stack>
|
||||
<Stack direction="column" justifyContent="space-between" flexWrap="wrap">
|
||||
<Typography>
|
||||
Average Market Time: <b>10</b> days on the market
|
||||
</Typography>
|
||||
<Typography>
|
||||
Price per Sq Ft: <b>$189</b> (neighborhood avg $175)
|
||||
</Typography>
|
||||
<Typography>
|
||||
Listing Activity: <b>5</b> homes sold in the last 30 days
|
||||
</Typography>
|
||||
<Typography>
|
||||
Compariable asking vs selling price: +$10k
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
|
||||
|
||||
</CardContent>
|
||||
|
||||
|
||||
</Card>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default MarketStatistics;
|
||||
@@ -0,0 +1,156 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { Card, CardContent, CardMedia, IconButton, ImageList, ImageListItem, ImageListItemBar, Stack, Typography } from '@mui/material';
|
||||
|
||||
function srcset(image: string, width: number, height: number, rows = 1, cols = 1) {
|
||||
return {
|
||||
src: `${image}?w=${width * cols}&h=${height * rows}&fit=crop&auto=format`,
|
||||
srcSet: `${image}?w=${width * cols}&h=${
|
||||
height * rows
|
||||
}&fit=crop&auto=format&dpr=2 2x`,
|
||||
};
|
||||
}
|
||||
|
||||
const PhotoGalleryCard = (): ReactElement => {
|
||||
const itemData = [
|
||||
{
|
||||
img: 'https://images.unsplash.com/photo-1551963831-b3b1ca40c98e',
|
||||
title: 'Breakfast',
|
||||
author: '@bkristastucchio',
|
||||
featured: true,
|
||||
},
|
||||
{
|
||||
img: 'https://images.unsplash.com/photo-1551782450-a2132b4ba21d',
|
||||
title: 'Burger',
|
||||
author: '@rollelflex_graphy726',
|
||||
},
|
||||
{
|
||||
img: 'https://images.unsplash.com/photo-1522770179533-24471fcdba45',
|
||||
title: 'Camera',
|
||||
author: '@helloimnik',
|
||||
},
|
||||
{
|
||||
img: 'https://images.unsplash.com/photo-1444418776041-9c7e33cc5a9c',
|
||||
title: 'Coffee',
|
||||
author: '@nolanissac',
|
||||
},
|
||||
{
|
||||
img: 'https://images.unsplash.com/photo-1533827432537-70133748f5c8',
|
||||
title: 'Hats',
|
||||
author: '@hjrc33',
|
||||
},
|
||||
{
|
||||
img: 'https://images.unsplash.com/photo-1558642452-9d2a7deb7f62',
|
||||
title: 'Honey',
|
||||
author: '@arwinneil',
|
||||
featured: true,
|
||||
},
|
||||
{
|
||||
img: 'https://images.unsplash.com/photo-1516802273409-68526ee1bdd6',
|
||||
title: 'Basketball',
|
||||
author: '@tjdragotta',
|
||||
},
|
||||
{
|
||||
img: 'https://images.unsplash.com/photo-1518756131217-31eb79b20e8f',
|
||||
title: 'Fern',
|
||||
author: '@katie_wasserman',
|
||||
},
|
||||
{
|
||||
img: 'https://images.unsplash.com/photo-1597645587822-e99fa5d45d25',
|
||||
title: 'Mushrooms',
|
||||
author: '@silverdalex',
|
||||
},
|
||||
{
|
||||
img: 'https://images.unsplash.com/photo-1567306301408-9b74779a11af',
|
||||
title: 'Tomato basil',
|
||||
author: '@shelleypauls',
|
||||
},
|
||||
{
|
||||
img: 'https://images.unsplash.com/photo-1471357674240-e1a485acb3e1',
|
||||
title: 'Sea star',
|
||||
author: '@peterlaster',
|
||||
},
|
||||
{
|
||||
img: 'https://images.unsplash.com/photo-1589118949245-7d38baf380d6',
|
||||
title: 'Bike',
|
||||
author: '@southside_customs',
|
||||
},
|
||||
];
|
||||
return(
|
||||
<Card
|
||||
sx={(theme) => ({
|
||||
boxShadow: theme.shadows[4],
|
||||
width: 1,
|
||||
height: 'auto',
|
||||
})}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
flex: '1 1 auto',
|
||||
padding: 0,
|
||||
':last-child': {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" flexWrap="wrap">
|
||||
<Typography variant="subtitle1" component="h3" minWidth={100} color="text.primary">
|
||||
Photo Gallery
|
||||
</Typography>
|
||||
|
||||
</Stack>
|
||||
<ImageList
|
||||
sx={{
|
||||
width: 500,
|
||||
height: 450,
|
||||
// Promote the list into its own layer in Chrome. This costs memory, but helps keeping high FPS.
|
||||
transform: 'translateZ(0)',
|
||||
}}
|
||||
rowHeight={200}
|
||||
gap={1}
|
||||
>
|
||||
{itemData.map((item) => {
|
||||
const cols = item.featured ? 2 : 1;
|
||||
const rows = item.featured ? 2 : 1;
|
||||
|
||||
return (
|
||||
<ImageListItem key={item.img} cols={cols} rows={rows}>
|
||||
<img
|
||||
{...srcset(item.img, 250, 200, rows, cols)}
|
||||
alt={item.title}
|
||||
loading="lazy"
|
||||
/>
|
||||
<ImageListItemBar
|
||||
sx={{
|
||||
background:
|
||||
'linear-gradient(to bottom, rgba(0,0,0,0.7) 0%, ' +
|
||||
'rgba(0,0,0,0.3) 70%, rgba(0,0,0,0) 100%)',
|
||||
}}
|
||||
title={item.title}
|
||||
position="top"
|
||||
actionIcon={
|
||||
<IconButton
|
||||
sx={{ color: 'white' }}
|
||||
aria-label={`star ${item.title}`}
|
||||
>
|
||||
|
||||
</IconButton>
|
||||
}
|
||||
actionPosition="left"
|
||||
/>
|
||||
</ImageListItem>
|
||||
);
|
||||
})}
|
||||
</ImageList>
|
||||
|
||||
|
||||
|
||||
|
||||
</CardContent>
|
||||
|
||||
|
||||
</Card>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default PhotoGalleryCard;
|
||||
@@ -0,0 +1,80 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { Card, CardContent, CardMedia, Divider, Stack, Typography } from '@mui/material';
|
||||
import Grid from '@mui/material/Unstable_Grid2';
|
||||
|
||||
const PropertyDetailsCard = (): ReactElement => {
|
||||
return(
|
||||
<Card
|
||||
sx={(theme) => ({
|
||||
boxShadow: theme.shadows[4],
|
||||
width: 1,
|
||||
height: 'auto',
|
||||
})}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
flex: '1 1 auto',
|
||||
padding: 0,
|
||||
':last-child': {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" flexWrap="wrap">
|
||||
<Typography variant="subtitle1" component="h3" minWidth={100} color="text.primary">
|
||||
Property Details
|
||||
</Typography>
|
||||
|
||||
</Stack>
|
||||
<Grid
|
||||
container
|
||||
>
|
||||
<Grid xs={6}>
|
||||
<Typography>
|
||||
Property Type: Single Family Home
|
||||
</Typography>
|
||||
|
||||
</Grid>
|
||||
<Grid xs={6}>
|
||||
<Typography>
|
||||
Year Built: 1998
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid xs={6}>
|
||||
<Typography>
|
||||
Lot Size: 0.25 acres
|
||||
</Typography>
|
||||
|
||||
</Grid>
|
||||
<Grid xs={6}>
|
||||
<Typography>
|
||||
Bedrooms: 3
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid xs={6}>
|
||||
<Typography>
|
||||
Bathrooms: 2
|
||||
</Typography>
|
||||
|
||||
</Grid>
|
||||
<Grid xs={6}>
|
||||
<Typography>
|
||||
Square Feet: 1,850
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Divider />
|
||||
<Typography>
|
||||
Beautifully maintained home in desirable neighborhood. Features updated kitchen with granite countertops, hardwood floors throughout main living areas, spacious master suite, and large backyard with deck. Excellent schools nearby.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
|
||||
|
||||
</Card>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default PropertyDetailsCard;
|
||||
@@ -0,0 +1,68 @@
|
||||
|
||||
import { ReactElement } from 'react';
|
||||
import { Card, CardContent, CardMedia, Stack, Typography } from '@mui/material';
|
||||
|
||||
type EducationInfoProps = {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const ProperyInfoCards = () => {
|
||||
return(
|
||||
<Stack direction={{sm:'row'}} justifyContent={{ sm: 'space-between' }} gap={3.75}>
|
||||
<PropertyInfo title={'1968 Greensboro Dr'} />
|
||||
|
||||
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const PropertyInfo = ({ title }: EducationInfoProps): ReactElement => {
|
||||
return(
|
||||
<Card
|
||||
sx={(theme) => ({
|
||||
boxShadow: theme.shadows[4],
|
||||
width: 1,
|
||||
height: 'auto',
|
||||
})}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
flex: '1 1 auto',
|
||||
padding: 0,
|
||||
':last-child': {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" flexWrap="wrap">
|
||||
<Typography variant="subtitle1" component="p" minWidth={100} color="text.primary">
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
</Stack>
|
||||
<Stack direction="column" justifyContent="space-between" flexWrap="wrap">
|
||||
<Typography>
|
||||
Estimated Home Value: <b>$700,500k</b>
|
||||
</Typography>
|
||||
<Typography>
|
||||
Estimated Savings: $24,000k
|
||||
</Typography>
|
||||
<Typography>
|
||||
Compariable Time on market: 5 days
|
||||
</Typography>
|
||||
<Typography>
|
||||
Compariable asking vs selling price: +$10k
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
|
||||
|
||||
</CardContent>
|
||||
|
||||
|
||||
</Card>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default PropertyInfo;
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { Card, CardContent, CardMedia, Stack, Typography } from '@mui/material';
|
||||
|
||||
const PropertyListingCard = (): ReactElement => {
|
||||
return(
|
||||
<Card
|
||||
sx={(theme) => ({
|
||||
boxShadow: theme.shadows[4],
|
||||
width: 1,
|
||||
height: 'auto',
|
||||
})}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
flex: '1 1 auto',
|
||||
padding: 0,
|
||||
':last-child': {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" flexWrap="wrap">
|
||||
<Typography variant="subtitle1" component="p" minWidth={100} color="text.primary">
|
||||
MLS Listing
|
||||
</Typography>
|
||||
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="space-between" flexWrap="wrap">
|
||||
<Typography>
|
||||
Zillow
|
||||
</Typography>
|
||||
<Typography>
|
||||
Redfin
|
||||
</Typography>
|
||||
<Typography>
|
||||
Realtor
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
|
||||
|
||||
</CardContent>
|
||||
|
||||
|
||||
</Card>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default PropertyListingCard;
|
||||
@@ -0,0 +1,191 @@
|
||||
import { ReactElement, useRef, useState } from 'react';
|
||||
import { Box, Button, Stack, Typography, useTheme } from '@mui/material';
|
||||
import EChartsReactCore from 'echarts-for-react/lib/core';
|
||||
|
||||
import { LineSeriesOption } from 'echarts';
|
||||
import RevenueChart from '../Sales/Revenue/RevenueChart';
|
||||
|
||||
const PropertyValueGraphCard = (): ReactElement => {
|
||||
const theme = useTheme();
|
||||
const chartRef = useRef<EChartsReactCore | null>(null);
|
||||
|
||||
const lineChartColors = [theme.palette.secondary.main, theme.palette.primary.main];
|
||||
|
||||
const legendData = [
|
||||
{ name: 'Loan Value', icon: 'circle' },
|
||||
{ name: 'Home Value', icon: 'circle' },
|
||||
];
|
||||
|
||||
const seriesData: LineSeriesOption[] = [
|
||||
{
|
||||
id: 1,
|
||||
data: [450000,
|
||||
449200,
|
||||
448400,
|
||||
447600,
|
||||
446800,
|
||||
446000,
|
||||
445200,
|
||||
444400,
|
||||
443600,
|
||||
442800,
|
||||
442000,
|
||||
441200,
|
||||
440400,
|
||||
439600,
|
||||
438800,
|
||||
438000,
|
||||
437200,
|
||||
436400,
|
||||
435600,
|
||||
434800,
|
||||
434000,
|
||||
433200,
|
||||
432400,
|
||||
431600,
|
||||
430800,
|
||||
430000],
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
color: lineChartColors[0],
|
||||
name: 'Loan Value',
|
||||
legendHoverLink: true,
|
||||
showSymbol: true,
|
||||
symbolSize: 12,
|
||||
lineStyle: {
|
||||
width: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: [450000,
|
||||
452000,
|
||||
453000,
|
||||
452000,
|
||||
452000,
|
||||
451000,
|
||||
450000,
|
||||
452000,
|
||||
452000,
|
||||
454000,
|
||||
453000,
|
||||
452000,
|
||||
452000,
|
||||
452000,
|
||||
451000,
|
||||
451000,
|
||||
451000,
|
||||
451000,
|
||||
450000,
|
||||
451000,
|
||||
452000,
|
||||
453000,
|
||||
454000,
|
||||
453000,
|
||||
455000,
|
||||
454000],
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
color: lineChartColors[1],
|
||||
name: 'Home Value',
|
||||
legendHoverLink: true,
|
||||
showSymbol: false,
|
||||
symbolSize: 12,
|
||||
lineStyle: {
|
||||
width: 5,
|
||||
},
|
||||
},
|
||||
];
|
||||
const [revenueAdType, setRevenueAdType] = useState<any>({
|
||||
'Loan Value': false,
|
||||
'Home Value': false,
|
||||
});
|
||||
|
||||
const toggleClicked = (name: string) => {
|
||||
setRevenueAdType((prevState: any) => ({
|
||||
...prevState,
|
||||
[name]: !prevState[name],
|
||||
}));
|
||||
};
|
||||
|
||||
const onChartLegendSelectChanged = (name: string) => {
|
||||
if (chartRef.current) {
|
||||
const instance = chartRef.current.getEchartsInstance();
|
||||
instance.dispatchAction({
|
||||
type: 'legendToggleSelect',
|
||||
name: name,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return(
|
||||
<Stack
|
||||
bgcolor="common.white"
|
||||
borderRadius={5}
|
||||
minHeight={460}
|
||||
height={1}
|
||||
mx="auto"
|
||||
boxShadow={theme.shadows[4]}
|
||||
>
|
||||
<Stack
|
||||
direction={{ sm: 'row' }}
|
||||
justifyContent={{ sm: 'space-between' }}
|
||||
alignItems={{ sm: 'center' }}
|
||||
gap={2}
|
||||
padding={3.75}
|
||||
>
|
||||
<Typography variant="h5" color="text.primary">
|
||||
Home and Loan Value
|
||||
</Typography>
|
||||
<Stack direction="row" gap={2}>
|
||||
{Array.isArray(seriesData) &&
|
||||
seriesData.map((dataItem, index) => (
|
||||
<Button
|
||||
key={dataItem.id}
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
//toggleClicked(dataItem.name as string);
|
||||
onChartLegendSelectChanged(dataItem.name as string);
|
||||
}}
|
||||
sx={{
|
||||
justifyContent: 'flex-start',
|
||||
p: 0,
|
||||
borderRadius: 1,
|
||||
opacity: revenueAdType[`${dataItem.name}`] ? 0.5 : 1,
|
||||
}}
|
||||
disableRipple
|
||||
>
|
||||
{' '}
|
||||
<Stack direction="row" alignItems="center" gap={1} width={1}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 13,
|
||||
height: 13,
|
||||
bgcolor: revenueAdType[`${dataItem.name}`]
|
||||
? 'action.disabled'
|
||||
: lineChartColors[index],
|
||||
borderRadius: 400,
|
||||
}}
|
||||
></Box>
|
||||
<Typography variant="body2" color="text.secondary" flex={1} textAlign={'left'}>
|
||||
{dataItem.name}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box flex={1}>
|
||||
<RevenueChart
|
||||
chartRef={chartRef}
|
||||
sx={{ minHeight: 1 }}
|
||||
seriesData={seriesData}
|
||||
legendData={legendData}
|
||||
colors={lineChartColors}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export default PropertyValueGraphCard;
|
||||
@@ -0,0 +1,196 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Stack,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import IconifyIcon from 'components/base/IconifyIcon';
|
||||
import { ReactElement, useRef, useState } from 'react';
|
||||
import EChartsReactCore from 'echarts-for-react/lib/core';
|
||||
import BuyersProfileChart from './BuyersProfileChart';
|
||||
import { PieDataItemOption } from 'echarts/types/src/chart/pie/PieSeries.js';
|
||||
|
||||
const BuyersProfile = (): ReactElement => {
|
||||
const theme = useTheme();
|
||||
const seriesData: PieDataItemOption[] = [
|
||||
{ value: 50, name: 'Male' },
|
||||
{ value: 35, name: 'Female' },
|
||||
{ value: 15, name: 'Others' },
|
||||
];
|
||||
const legendData = [
|
||||
{ name: 'Male', icon: 'circle' },
|
||||
{ name: 'Female', icon: 'circle' },
|
||||
{ name: 'Others', icon: 'circle' },
|
||||
];
|
||||
const pieChartColors = [
|
||||
theme.palette.primary.main,
|
||||
theme.palette.secondary.main,
|
||||
theme.palette.error.main,
|
||||
];
|
||||
|
||||
const chartRef = useRef<EChartsReactCore | null>(null);
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const [buyerGenderType, setBuyerGenderType] = useState<any>({
|
||||
Male: false,
|
||||
Female: false,
|
||||
Others: false,
|
||||
});
|
||||
|
||||
const toggleClicked = (name: string) => {
|
||||
setBuyerGenderType((prevState: any) => ({
|
||||
...prevState,
|
||||
[name]: !prevState[name],
|
||||
}));
|
||||
};
|
||||
const handleClick = (event: any) => {
|
||||
setAnchorEl(event.target);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const onChartLegendSelectChanged = (name: string) => {
|
||||
if (chartRef.current) {
|
||||
const instance = chartRef.current.getEchartsInstance();
|
||||
instance.dispatchAction({
|
||||
type: 'legendToggleSelect',
|
||||
name: name,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
bgcolor: 'common.white',
|
||||
borderRadius: 5,
|
||||
height: 1,
|
||||
flex: '1 1 auto',
|
||||
width: { xs: 'auto', sm: 0.5, lg: 'auto' },
|
||||
boxShadow: (theme) => theme.shadows[4],
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" padding={2.5}>
|
||||
<Typography variant="subtitle1" color="text.primary">
|
||||
Buyers Profile
|
||||
</Typography>
|
||||
<IconButton
|
||||
id="buyers-profile-button"
|
||||
aria-controls={open ? 'buyers-profile-menu' : undefined}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
onClick={handleClick}
|
||||
sx={{
|
||||
bgcolor: open ? 'action.active' : 'transparent',
|
||||
p: 1,
|
||||
width: 36,
|
||||
height: 36,
|
||||
'&:hover': {
|
||||
bgcolor: 'action.active',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IconifyIcon icon="ph:dots-three-outline-fill" color="text.secondary" />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="buyers-profile-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
MenuListProps={{
|
||||
'aria-labelledby': 'buyers-profile-button',
|
||||
}}
|
||||
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
||||
>
|
||||
<MenuItem onClick={handleClose}>
|
||||
<Typography variant="body1" component="p">
|
||||
Edit
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleClose}>
|
||||
<Typography variant="body1" component="p" color="error.main">
|
||||
Delete
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction={{ xs: 'row', sm: 'column', md: 'row' }}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
flex={1}
|
||||
gap={2}
|
||||
padding={(theme) => theme.spacing(0, 2.5, 2.5)}
|
||||
>
|
||||
<BuyersProfileChart
|
||||
chartRef={chartRef}
|
||||
seriesData={seriesData}
|
||||
legendData={legendData}
|
||||
colors={pieChartColors}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flex: '1 1 0%',
|
||||
width: 177,
|
||||
maxHeight: 177,
|
||||
}}
|
||||
/>
|
||||
<Stack
|
||||
spacing={2}
|
||||
sx={{
|
||||
width: { xs: 0.5, sm: 'auto', md: 'auto', lg: 'auto' },
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{Array.isArray(seriesData) &&
|
||||
seriesData.map((dataItem, index) => (
|
||||
<Button
|
||||
key={dataItem.name}
|
||||
variant="text"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
toggleClicked(dataItem.name as string);
|
||||
onChartLegendSelectChanged(dataItem.name as string);
|
||||
}}
|
||||
sx={{
|
||||
justifyContent: 'flex-start',
|
||||
p: 0,
|
||||
pr: 1,
|
||||
borderRadius: 1,
|
||||
opacity: buyerGenderType[`${dataItem.name}`] ? 0.5 : 1,
|
||||
}}
|
||||
disableRipple
|
||||
>
|
||||
<Stack direction="row" alignItems="center" gap={1} width={1}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
bgcolor: buyerGenderType[`${dataItem.name}`]
|
||||
? 'action.disabled'
|
||||
: pieChartColors[index],
|
||||
borderRadius: 400,
|
||||
}}
|
||||
/>
|
||||
<Typography variant="body1" color="text.secondary" textAlign="left" flex={1}>
|
||||
{dataItem.name}
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.primary">
|
||||
{dataItem.value}%
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default BuyersProfile;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { SxProps, useTheme } from '@mui/material';
|
||||
import ReactEchart from 'components/base/ReactEchart';
|
||||
import * as echarts from 'echarts';
|
||||
import { EChartsOption } from 'echarts-for-react';
|
||||
import EChartsReactCore from 'echarts-for-react/lib/core';
|
||||
import { PieDataItemOption } from 'echarts/types/src/chart/pie/PieSeries.js';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
type BuyersProfileChartProps = {
|
||||
chartRef: React.MutableRefObject<EChartsReactCore | null>;
|
||||
seriesData?: PieDataItemOption[];
|
||||
legendData?: any;
|
||||
colors?: string[];
|
||||
sx?: SxProps;
|
||||
};
|
||||
|
||||
const BuyersProfileChart = ({
|
||||
chartRef,
|
||||
seriesData,
|
||||
legendData,
|
||||
colors,
|
||||
...rest
|
||||
}: BuyersProfileChartProps) => {
|
||||
const theme = useTheme();
|
||||
const option: EChartsOption = useMemo(
|
||||
() => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b} : {c}%',
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
data: legendData,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Buyers Profile',
|
||||
type: 'pie',
|
||||
radius: ['65%', '90%'],
|
||||
color: colors,
|
||||
avoidLabelOverlap: true,
|
||||
startAngle: -30,
|
||||
clockwise: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
scaleSize: 0,
|
||||
},
|
||||
labelLine: {
|
||||
show: true,
|
||||
},
|
||||
data: seriesData,
|
||||
},
|
||||
],
|
||||
}),
|
||||
[theme],
|
||||
);
|
||||
|
||||
return <ReactEchart ref={chartRef} option={option} echarts={echarts} {...rest} />;
|
||||
};
|
||||
|
||||
export default BuyersProfileChart;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Avatar, IconButton, Link, ListItem, Stack, Tooltip, Typography } from '@mui/material';
|
||||
import IconifyIcon from 'components/base/IconifyIcon';
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
type CustomerItemProps = {
|
||||
name: string;
|
||||
country: string;
|
||||
avatar: string;
|
||||
};
|
||||
|
||||
const CustomerItem = ({ name, country, avatar }: CustomerItemProps): ReactElement => {
|
||||
const firstName = name.split(' ')[0];
|
||||
return (
|
||||
<ListItem
|
||||
sx={(theme) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
padding: theme.spacing(1.25, 2.5),
|
||||
})}
|
||||
>
|
||||
<Stack direction="row" gap={1.5} component={Link}>
|
||||
<Tooltip title={firstName} placement="top" arrow enterDelay={0} leaveDelay={0}>
|
||||
<Avatar src={avatar} />
|
||||
</Tooltip>
|
||||
<Stack component="div">
|
||||
<Typography variant="body1" color="text.primary">
|
||||
{name}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{country}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<IconButton>
|
||||
<IconifyIcon icon="mingcute:mail-fill" color="primary.main" width={16} height={16} />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerItem;
|
||||
@@ -0,0 +1,93 @@
|
||||
import { ReactElement, useState } from 'react';
|
||||
import { Box, IconButton, Menu, MenuItem, Stack, Typography } from '@mui/material';
|
||||
|
||||
import IconifyIcon from 'components/base/IconifyIcon';
|
||||
import { customerList } from 'data/customers-list';
|
||||
import CustomerItem from './CustomerItem';
|
||||
|
||||
const NewCustomers = (): ReactElement => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const handleClick = (event: any) => {
|
||||
setAnchorEl(event.target);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: 'common.white',
|
||||
borderRadius: 5,
|
||||
height: 1,
|
||||
flex: '1 1 auto',
|
||||
width: { xs: 'auto', sm: 0.5, lg: 'auto' },
|
||||
boxShadow: (theme) => theme.shadows[4],
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" padding={2.5}>
|
||||
<Typography variant="subtitle1" color="text.primary">
|
||||
New Customers
|
||||
</Typography>
|
||||
<IconButton
|
||||
id="new-customers-button"
|
||||
aria-controls={open ? 'new-customers-menu' : undefined}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
onClick={handleClick}
|
||||
sx={{
|
||||
bgcolor: open ? 'action.active' : 'transparent',
|
||||
padding: 1,
|
||||
width: 36,
|
||||
height: 36,
|
||||
'&:hover': {
|
||||
bgcolor: 'action.active',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IconifyIcon icon="ph:dots-three-outline-fill" color="text.secondary" />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="new-customers-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
MenuListProps={{
|
||||
'aria-labelledby': 'new-customers-button',
|
||||
}}
|
||||
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
||||
>
|
||||
<MenuItem onClick={handleClose}>
|
||||
<Typography variant="body1" component="p">
|
||||
Last Week
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleClose}>
|
||||
<Typography variant="body1" component="p">
|
||||
Last Month
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleClose}>
|
||||
<Typography variant="body1" component="p">
|
||||
Last Year
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Stack>
|
||||
<Stack pb={1.25}>
|
||||
{customerList.map((customer) => (
|
||||
<CustomerItem
|
||||
key={customer.id}
|
||||
name={customer.name}
|
||||
country={customer.country}
|
||||
avatar={customer.avatar}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewCustomers;
|
||||
@@ -0,0 +1,141 @@
|
||||
import { ReactElement, useRef, useState } from 'react';
|
||||
import { Box, Button, Stack, Typography, useTheme } from '@mui/material';
|
||||
import EChartsReactCore from 'echarts-for-react/lib/core';
|
||||
import RevenueChart from './RevenueChart';
|
||||
import { LineSeriesOption } from 'echarts';
|
||||
|
||||
const Revenue = (): ReactElement => {
|
||||
const theme = useTheme();
|
||||
const chartRef = useRef<EChartsReactCore | null>(null);
|
||||
|
||||
const lineChartColors = [theme.palette.secondary.main, theme.palette.primary.main];
|
||||
|
||||
const legendData = [
|
||||
{ name: 'Google ads', icon: 'circle' },
|
||||
{ name: 'Facebook ads', icon: 'circle' },
|
||||
];
|
||||
|
||||
const seriesData: LineSeriesOption[] = [
|
||||
{
|
||||
id: 1,
|
||||
data: [65, 210, 175, 140, 105, 20, 120, 20],
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
color: lineChartColors[0],
|
||||
name: 'Google ads',
|
||||
legendHoverLink: true,
|
||||
showSymbol: true,
|
||||
symbolSize: 12,
|
||||
lineStyle: {
|
||||
width: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
data: [20, 125, 100, 30, 150, 300, 90, 180],
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
color: lineChartColors[1],
|
||||
name: 'Facebook ads',
|
||||
legendHoverLink: true,
|
||||
showSymbol: false,
|
||||
symbolSize: 12,
|
||||
lineStyle: {
|
||||
width: 5,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const onChartLegendSelectChanged = (name: string) => {
|
||||
if (chartRef.current) {
|
||||
const instance = chartRef.current.getEchartsInstance();
|
||||
instance.dispatchAction({
|
||||
type: 'legendToggleSelect',
|
||||
name: name,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const [revenueAdType, setRevenueAdType] = useState<any>({
|
||||
'Google ads': false,
|
||||
'Facebook ads': false,
|
||||
});
|
||||
|
||||
const toggleClicked = (name: string) => {
|
||||
setRevenueAdType((prevState: any) => ({
|
||||
...prevState,
|
||||
[name]: !prevState[name],
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
bgcolor="common.white"
|
||||
borderRadius={5}
|
||||
minHeight={460}
|
||||
height={1}
|
||||
mx="auto"
|
||||
boxShadow={theme.shadows[4]}
|
||||
>
|
||||
<Stack
|
||||
direction={{ sm: 'row' }}
|
||||
justifyContent={{ sm: 'space-between' }}
|
||||
alignItems={{ sm: 'center' }}
|
||||
gap={2}
|
||||
padding={3.75}
|
||||
>
|
||||
<Typography variant="h5" color="text.primary">
|
||||
Revenue
|
||||
</Typography>
|
||||
<Stack direction="row" gap={2}>
|
||||
{Array.isArray(seriesData) &&
|
||||
seriesData.map((dataItem, index) => (
|
||||
<Button
|
||||
key={dataItem.id}
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
toggleClicked(dataItem.name as string);
|
||||
onChartLegendSelectChanged(dataItem.name as string);
|
||||
}}
|
||||
sx={{
|
||||
justifyContent: 'flex-start',
|
||||
p: 0,
|
||||
borderRadius: 1,
|
||||
opacity: revenueAdType[`${dataItem.name}`] ? 0.5 : 1,
|
||||
}}
|
||||
disableRipple
|
||||
>
|
||||
{' '}
|
||||
<Stack direction="row" alignItems="center" gap={1} width={1}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 13,
|
||||
height: 13,
|
||||
bgcolor: revenueAdType[`${dataItem.name}`]
|
||||
? 'action.disabled'
|
||||
: lineChartColors[index],
|
||||
borderRadius: 400,
|
||||
}}
|
||||
></Box>
|
||||
<Typography variant="body2" color="text.secondary" flex={1} textAlign={'left'}>
|
||||
{dataItem.name}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box flex={1}>
|
||||
<RevenueChart
|
||||
chartRef={chartRef}
|
||||
sx={{ minHeight: 1 }}
|
||||
seriesData={seriesData}
|
||||
legendData={legendData}
|
||||
colors={lineChartColors}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Revenue;
|
||||
@@ -0,0 +1,91 @@
|
||||
import { SxProps, useTheme } from '@mui/material';
|
||||
import ReactEchart from 'components/base/ReactEchart';
|
||||
import * as echarts from 'echarts';
|
||||
import EChartsReactCore from 'echarts-for-react/lib/core';
|
||||
import { LineSeriesOption } from 'echarts';
|
||||
import { useMemo } from 'react';
|
||||
import { EChartsOption } from 'echarts-for-react';
|
||||
|
||||
type RevenueChartProps = {
|
||||
chartRef: React.MutableRefObject<EChartsReactCore | null>;
|
||||
seriesData?: LineSeriesOption[];
|
||||
legendData?: any;
|
||||
colors?: string[];
|
||||
sx?: SxProps;
|
||||
};
|
||||
|
||||
const RevenueChart = ({ chartRef, seriesData, legendData, colors, ...rest }: RevenueChartProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const option: EChartsOption = useMemo(
|
||||
() => ({
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August'],
|
||||
boundaryGap: false,
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: theme.palette.divider,
|
||||
width: 1,
|
||||
type: 'dashed',
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
show: true,
|
||||
padding: 30,
|
||||
color: theme.palette.text.secondary,
|
||||
formatter: (value: any) => value.slice(0, 3),
|
||||
fontFamily: theme.typography.body2.fontFamily,
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min:420000,
|
||||
max:480000,
|
||||
splitNumber: 4,
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
show: true,
|
||||
color: theme.palette.text.secondary,
|
||||
align: 'center',
|
||||
padding: [0, 20, 0, 0],
|
||||
fontFamily: theme.typography.body2.fontFamily,
|
||||
},
|
||||
splitLine: {
|
||||
interval: 5,
|
||||
lineStyle: {
|
||||
color: theme.palette.divider,
|
||||
width: 1,
|
||||
type: 'dashed',
|
||||
},
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: 60,
|
||||
right: 30,
|
||||
top: 30,
|
||||
bottom: 90,
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'axis',
|
||||
valueFormatter: (value: any) => '$' + value.toFixed(0),
|
||||
},
|
||||
series: seriesData,
|
||||
}),
|
||||
[theme],
|
||||
);
|
||||
|
||||
return <ReactEchart ref={chartRef} echarts={echarts} option={option} {...rest} />;
|
||||
};
|
||||
|
||||
export default RevenueChart;
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { Card, CardContent, CardMedia, Stack, Typography } from '@mui/material';
|
||||
import IconifyIcon from 'components/base/IconifyIcon';
|
||||
import Image from 'components/base/Image';
|
||||
import { currencyFormat } from 'helpers/format-functions';
|
||||
|
||||
type SaleInfoProps = {
|
||||
image?: string;
|
||||
title: string;
|
||||
sales: number;
|
||||
increment: number;
|
||||
date?: string;
|
||||
};
|
||||
|
||||
const SaleInfo = ({ image, title, sales, increment, date }: SaleInfoProps): ReactElement => {
|
||||
return (
|
||||
<Card
|
||||
sx={(theme) => ({
|
||||
boxShadow: theme.shadows[4],
|
||||
width: 1,
|
||||
height: 'auto',
|
||||
})}
|
||||
>
|
||||
<CardMedia
|
||||
sx={{
|
||||
maxWidth: 70,
|
||||
maxHeight: 70,
|
||||
}}
|
||||
>
|
||||
<Image src={`${image}`} width={1} height={1} />
|
||||
</CardMedia>
|
||||
<CardContent
|
||||
sx={{
|
||||
flex: '1 1 auto',
|
||||
padding: 0,
|
||||
':last-child': {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" flexWrap="wrap">
|
||||
<Typography variant="subtitle1" component="p" minWidth={100} color="text.primary">
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="body2" component="p" color="text.secondary">
|
||||
{date}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Typography variant="body1" component="p" color="text.secondary">
|
||||
{currencyFormat(sales)}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="primary.main"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={1}
|
||||
whiteSpace={'nowrap'}
|
||||
>
|
||||
<IconifyIcon icon="ph:trend-up-fill" width={16} height={16} />
|
||||
{`+${increment}%`} last month
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SaleInfo;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Stack } from '@mui/material';
|
||||
import { saleInfoData } from 'data/sale-info-data';
|
||||
import SaleInfo from './SaleInfo';
|
||||
|
||||
const SaleInfoCards = () => {
|
||||
return (
|
||||
<Stack direction={{ sm: 'row' }} justifyContent={{ sm: 'space-between' }} gap={3.75}>
|
||||
{saleInfoData.map((saleInfoDataItem) => (
|
||||
<SaleInfo
|
||||
key={saleInfoDataItem.id}
|
||||
title={saleInfoDataItem.title}
|
||||
image={saleInfoDataItem.image}
|
||||
sales={saleInfoDataItem.sales}
|
||||
increment={saleInfoDataItem.increment}
|
||||
date={saleInfoDataItem.date}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SaleInfoCards;
|
||||
@@ -0,0 +1,80 @@
|
||||
import { PaginationItem, TablePaginationProps, Typography } from '@mui/material';
|
||||
import {
|
||||
GridPagination,
|
||||
gridExpandedRowCountSelector,
|
||||
gridPageCountSelector,
|
||||
gridPaginationRowRangeSelector,
|
||||
useGridApiContext,
|
||||
useGridSelector,
|
||||
} from '@mui/x-data-grid';
|
||||
import MuiPagination from '@mui/material/Pagination';
|
||||
import { useBreakpoints } from 'providers/BreakpointsProvider';
|
||||
|
||||
function Pagination({
|
||||
page,
|
||||
className,
|
||||
}: Pick<TablePaginationProps, 'page' | 'onPageChange' | 'className' | 'ref'>) {
|
||||
const apiRef = useGridApiContext();
|
||||
const { down } = useBreakpoints();
|
||||
const belowSmallScreen = down('sm');
|
||||
|
||||
const pageCount = useGridSelector(apiRef, gridPageCountSelector);
|
||||
const available = useGridSelector(apiRef, gridExpandedRowCountSelector);
|
||||
const paginationRowRange = useGridSelector(apiRef, gridPaginationRowRangeSelector);
|
||||
|
||||
return (
|
||||
<>
|
||||
{pageCount !== 0 ? (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
mr="auto"
|
||||
ml={belowSmallScreen ? 'auto' : ''}
|
||||
>
|
||||
Showing {(paginationRowRange?.firstRowIndex as number) + 1} -{' '}
|
||||
{(paginationRowRange?.lastRowIndex as number) + 1} of {available} Products
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
mr="auto"
|
||||
ml={belowSmallScreen ? 'auto' : ''}
|
||||
>
|
||||
Showing 0 - 0 of {available} Products
|
||||
</Typography>
|
||||
)}
|
||||
<MuiPagination
|
||||
color="primary"
|
||||
className={className}
|
||||
count={pageCount}
|
||||
page={page + 1}
|
||||
onChange={(_event, newPage) => apiRef.current.setPage(newPage - 1)}
|
||||
renderItem={(item) => (
|
||||
<PaginationItem
|
||||
{...item}
|
||||
slots={{
|
||||
previous: () => <>Prev</>,
|
||||
next: () => <>Next</>,
|
||||
}}
|
||||
sx={(theme) => ({
|
||||
'&.Mui-selected': {
|
||||
color: theme.palette.common.white,
|
||||
},
|
||||
'&.Mui-disabled': {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
sx={{
|
||||
mx: { xs: 'auto', sm: 'initial' },
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CustomPagination(props: object) {
|
||||
return <GridPagination ActionsComponent={Pagination} {...props} />;
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import { ChangeEvent, ReactElement, useMemo, useState } from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
Divider,
|
||||
InputAdornment,
|
||||
LinearProgress,
|
||||
Link,
|
||||
Stack,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
debounce,
|
||||
} from '@mui/material';
|
||||
import { DataGrid, GridApi, GridColDef, GridSlots, useGridApiRef } from '@mui/x-data-grid';
|
||||
import IconifyIcon from 'components/base/IconifyIcon';
|
||||
import { DataRow, rows } from 'data/products';
|
||||
import CustomPagination from './CustomPagination';
|
||||
import { currencyFormat } from 'helpers/format-functions';
|
||||
|
||||
const columns: GridColDef<DataRow>[] = [
|
||||
{
|
||||
field: 'id',
|
||||
headerName: 'ID',
|
||||
},
|
||||
{
|
||||
field: 'product',
|
||||
headerName: 'Product',
|
||||
flex: 1,
|
||||
minWidth: 182.9625,
|
||||
valueGetter: (params: any) => {
|
||||
return params.title + ' ' + params.subtitle;
|
||||
},
|
||||
renderCell: (params: any) => {
|
||||
return (
|
||||
<Stack direction="row" spacing={1.5} alignItems="center" component={Link} href="#!">
|
||||
<Tooltip title={params.row.product.title} placement="top" arrow>
|
||||
<Avatar src={params.row.product.avatar} sx={{ objectFit: 'cover' }} />
|
||||
</Tooltip>
|
||||
<Stack direction="column" spacing={0.5} justifyContent="space-between">
|
||||
<Typography variant="body1" color="text.primary">
|
||||
{params.row.product.title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{params.row.product.subtitle}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
sortComparator: (v1: string, v2: string) => v1.localeCompare(v2),
|
||||
},
|
||||
{
|
||||
field: 'orders',
|
||||
headerName: 'Orders',
|
||||
flex: 0.75,
|
||||
minWidth: 137.221875,
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
headerName: 'Price',
|
||||
flex: 0.75,
|
||||
minWidth: 137.221875,
|
||||
valueGetter: (params: any) => {
|
||||
return currencyFormat(params);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'adsSpent',
|
||||
headerName: 'Ads Spent',
|
||||
flex: 0.75,
|
||||
minWidth: 137.221875,
|
||||
valueGetter: (params: any) => {
|
||||
return currencyFormat(params, { minimumFractionDigits: 3 });
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'refunds',
|
||||
headerName: 'Refunds',
|
||||
flex: 0.75,
|
||||
minWidth: 137.221875,
|
||||
renderCell: ({ row: { refunds } }: any) => {
|
||||
if (refunds > 0) return `> ${refunds}`;
|
||||
else return `< ${-refunds}`;
|
||||
},
|
||||
filterable: false,
|
||||
},
|
||||
];
|
||||
|
||||
const TopSellingProduct = (): ReactElement => {
|
||||
const apiRef = useGridApiRef<GridApi>();
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const visibleColumns = useMemo(
|
||||
() =>
|
||||
columns
|
||||
.filter((column) => column.field !== 'id')
|
||||
.map((column) => {
|
||||
if (column.field === 'refunds') {
|
||||
return {
|
||||
...column,
|
||||
getApplyQuickFilterFn: undefined,
|
||||
filterable: false,
|
||||
};
|
||||
}
|
||||
return column;
|
||||
}),
|
||||
[columns],
|
||||
);
|
||||
|
||||
const handleGridSearch = useMemo(() => {
|
||||
return debounce((searchValue) => {
|
||||
apiRef.current.setQuickFilterValues(
|
||||
searchValue.split(' ').filter((word: any) => word !== ''),
|
||||
);
|
||||
}, 250);
|
||||
}, [apiRef]);
|
||||
|
||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const searchValue = event.currentTarget.value;
|
||||
setSearch(searchValue);
|
||||
handleGridSearch(searchValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
bgcolor="background.paper"
|
||||
borderRadius={5}
|
||||
width={1}
|
||||
boxShadow={(theme) => theme.shadows[4]}
|
||||
height={1}
|
||||
>
|
||||
<Stack
|
||||
direction={{ sm: 'row' }}
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
padding={3.75}
|
||||
gap={3.75}
|
||||
>
|
||||
<Typography variant="h5" color="text.primary">
|
||||
Top Selling Product
|
||||
</Typography>
|
||||
<TextField
|
||||
variant="filled"
|
||||
placeholder="Search..."
|
||||
id="search-input"
|
||||
name="table-search-input"
|
||||
onChange={handleChange}
|
||||
value={search}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end" sx={{ width: 24, height: 24 }}>
|
||||
<IconifyIcon icon="mdi:search" width={1} height={1} />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack height={1}>
|
||||
<DataGrid
|
||||
apiRef={apiRef}
|
||||
columns={visibleColumns}
|
||||
rows={rows}
|
||||
getRowHeight={() => 70}
|
||||
hideFooterSelectedRowCount
|
||||
disableColumnResize
|
||||
disableColumnSelector
|
||||
disableRowSelectionOnClick
|
||||
rowSelection={false}
|
||||
initialState={{
|
||||
pagination: { paginationModel: { pageSize: 5, page: 0 } },
|
||||
columns: {
|
||||
columnVisibilityModel: {
|
||||
id: false,
|
||||
},
|
||||
},
|
||||
}}
|
||||
pageSizeOptions={[5]}
|
||||
onResize={() => {
|
||||
apiRef.current.autosizeColumns({
|
||||
includeOutliers: true,
|
||||
expand: true,
|
||||
});
|
||||
}}
|
||||
slots={{
|
||||
loadingOverlay: LinearProgress as GridSlots['loadingOverlay'],
|
||||
pagination: CustomPagination,
|
||||
noRowsOverlay: () => <section>No rows available</section>,
|
||||
}}
|
||||
sx={{
|
||||
height: 1,
|
||||
width: 1,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopSellingProduct;
|
||||
@@ -0,0 +1,137 @@
|
||||
import { ReactElement, useMemo, useRef, useState } from 'react';
|
||||
import { Box, Button, Divider, Stack, Typography, useTheme } from '@mui/material';
|
||||
import EChartsReactCore from 'echarts-for-react/lib/core';
|
||||
import { PieDataItemOption } from 'echarts/types/src/chart/pie/PieSeries.js';
|
||||
import WebsiteVisitorsChart from './WebsiteVisitorsChart';
|
||||
|
||||
const WebsiteVisitors = (): ReactElement => {
|
||||
const theme = useTheme();
|
||||
|
||||
const seriesData: PieDataItemOption[] = [
|
||||
{ value: 6840, name: 'Direct' },
|
||||
{ value: 3960, name: 'Organic' },
|
||||
{ value: 2160, name: 'Paid' },
|
||||
{ value: 5040, name: 'Social' },
|
||||
];
|
||||
|
||||
const legendData = [
|
||||
{ name: 'Direct', icon: 'circle' },
|
||||
{ name: 'Organic', icon: 'circle' },
|
||||
{ name: 'Paid', icon: 'circle' },
|
||||
{ name: 'Social', icon: 'circle' },
|
||||
];
|
||||
|
||||
const pieChartColors = [
|
||||
theme.palette.primary.main,
|
||||
theme.palette.secondary.main,
|
||||
theme.palette.info.main,
|
||||
theme.palette.error.main,
|
||||
];
|
||||
|
||||
const chartRef = useRef<EChartsReactCore | null>(null);
|
||||
const onChartLegendSelectChanged = (name: string) => {
|
||||
if (chartRef.current) {
|
||||
const instance = chartRef.current.getEchartsInstance();
|
||||
instance.dispatchAction({
|
||||
type: 'legendToggleSelect',
|
||||
name: name,
|
||||
});
|
||||
}
|
||||
};
|
||||
const [visitorType, setVisitorType] = useState<any>({
|
||||
Direct: false,
|
||||
Organic: false,
|
||||
Paid: false,
|
||||
Social: false,
|
||||
});
|
||||
|
||||
const toggleClicked = (name: string) => {
|
||||
setVisitorType((prevState: any) => ({
|
||||
...prevState,
|
||||
[name]: !prevState[name],
|
||||
}));
|
||||
};
|
||||
const totalVisitors = useMemo(
|
||||
() => seriesData.reduce((acc: number, next: any) => acc + next.value, 0),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: 'common.white',
|
||||
borderRadius: 5,
|
||||
height: 'min-content',
|
||||
boxShadow: theme.shadows[4],
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" color="text.primary" p={2.5}>
|
||||
Website Visitors
|
||||
</Typography>
|
||||
<Stack direction={{ xs: 'column', sm: 'row', md: 'column' }}>
|
||||
<Stack direction="row" justifyContent="center" flex={'1 1 0%'}>
|
||||
<WebsiteVisitorsChart
|
||||
chartRef={chartRef}
|
||||
seriesData={seriesData}
|
||||
colors={pieChartColors}
|
||||
legendData={legendData}
|
||||
sx={{
|
||||
width: 222,
|
||||
maxHeight: 222,
|
||||
mx: 'auto',
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
spacing={1}
|
||||
divider={<Divider />}
|
||||
sx={{ px: 2.5, py: 2.5 }}
|
||||
justifyContent="center"
|
||||
alignItems="stretch"
|
||||
flex={'1 1 0%'}
|
||||
>
|
||||
{Array.isArray(seriesData) &&
|
||||
seriesData.map((dataItem, index) => (
|
||||
<Button
|
||||
key={dataItem.name}
|
||||
variant="text"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
toggleClicked(dataItem.name as string);
|
||||
onChartLegendSelectChanged(dataItem.name as string);
|
||||
}}
|
||||
sx={{
|
||||
justifyContent: 'flex-start',
|
||||
p: 0,
|
||||
borderRadius: 1,
|
||||
opacity: visitorType[`${dataItem.name}`] ? 0.5 : 1,
|
||||
}}
|
||||
disableRipple
|
||||
>
|
||||
<Stack direction="row" alignItems="center" gap={1} width={1}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
bgcolor: visitorType[`${dataItem.name}`]
|
||||
? 'action.disabled'
|
||||
: pieChartColors[index],
|
||||
borderRadius: 400,
|
||||
}}
|
||||
></Box>
|
||||
<Typography variant="body1" color="text.secondary" flex={1} textAlign={'left'}>
|
||||
{dataItem.name}
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.primary">
|
||||
{((parseInt(`${dataItem.value}`) / totalVisitors) * 100).toFixed(0)}%
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebsiteVisitors;
|
||||
@@ -0,0 +1,72 @@
|
||||
import { SxProps, useTheme } from '@mui/material';
|
||||
import ReactEchart from 'components/base/ReactEchart';
|
||||
import * as echarts from 'echarts';
|
||||
import EChartsReactCore from 'echarts-for-react/lib/core';
|
||||
import { PieDataItemOption } from 'echarts/types/src/chart/pie/PieSeries.js';
|
||||
import { useMemo } from 'react';
|
||||
import { EChartsOption } from 'echarts-for-react';
|
||||
|
||||
type WebsiteVisitorsChartProps = {
|
||||
chartRef: React.MutableRefObject<EChartsReactCore | null>;
|
||||
seriesData?: PieDataItemOption[];
|
||||
legendData?: any;
|
||||
colors?: string[];
|
||||
sx?: SxProps;
|
||||
};
|
||||
|
||||
const WebsiteVisitorsChart = ({
|
||||
chartRef,
|
||||
seriesData,
|
||||
legendData,
|
||||
colors,
|
||||
...rest
|
||||
}: WebsiteVisitorsChartProps) => {
|
||||
const theme = useTheme();
|
||||
const option: EChartsOption = useMemo(
|
||||
() => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
data: legendData,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Website Visitors',
|
||||
type: 'pie',
|
||||
radius: ['65%', '80%'],
|
||||
avoidLabelOverlap: true,
|
||||
startAngle: 0,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: theme.palette.common.white,
|
||||
borderWidth: 2,
|
||||
},
|
||||
color: colors,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 30,
|
||||
fontWeight: 'bold',
|
||||
formatter: `{b}`,
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: seriesData,
|
||||
},
|
||||
],
|
||||
}),
|
||||
[theme],
|
||||
);
|
||||
|
||||
return <ReactEchart ref={chartRef} option={option} echarts={echarts} {...rest} />;
|
||||
};
|
||||
|
||||
export default WebsiteVisitorsChart;
|
||||
66
ditch-the-agent/src/contexts/AuthContext.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { createContext, ReactNode, useState, useEffect } from "react"
|
||||
|
||||
|
||||
type AuthProviderProps = {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
type IAuthContext = {
|
||||
authenticated: boolean;
|
||||
setAuthentication: (newState: boolean) => void;
|
||||
needsNewPassword: boolean;
|
||||
setNeedsNewPassword: (newState: boolean) => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const initialValues = {
|
||||
authenticated: false,
|
||||
setAuthentication: () => {},
|
||||
needsNewPassword: false,
|
||||
setNeedsNewPassword: () => {},
|
||||
loading: true,
|
||||
}
|
||||
|
||||
const AuthContext = createContext<IAuthContext>(initialValues);
|
||||
|
||||
|
||||
const AuthProvider = ({children}: AuthProviderProps) => {
|
||||
|
||||
|
||||
const [ authenticated, setAuthentication ] = useState(initialValues.authenticated);
|
||||
const [loading, setLoading] = useState(true); // Add a loading state
|
||||
|
||||
useEffect(() => {
|
||||
console.log('we are in the auth provider')
|
||||
const accessToken = localStorage.getItem('access_token');
|
||||
if (accessToken) {
|
||||
const decodedToken = jwtDecode(accessToken)
|
||||
//console.log(decodedToken)
|
||||
if(decodedToken.exp){
|
||||
//console.log(decodedToken.exp * 1000)
|
||||
//console.log(Date.now())
|
||||
if (decodedToken.exp * 1000> Date.now()) {
|
||||
console.log('We are setting that we are authenticated')
|
||||
setAuthentication(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
setLoading(false);
|
||||
|
||||
|
||||
}, [])
|
||||
//console.log(authenticated)
|
||||
const [ needsNewPassword, setNeedsNewPassword] = useState(initialValues.needsNewPassword)
|
||||
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ authenticated, setAuthentication, needsNewPassword, setNeedsNewPassword, loading}}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export { AuthContext, AuthProvider }
|
||||
38
ditch-the-agent/src/data/customers-list.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import leatrice from 'assets/new-customers/leatrice.png';
|
||||
import roselle from 'assets/new-customers/roselle.jpg';
|
||||
import darron from 'assets/new-customers/darron.png';
|
||||
import jone from 'assets/new-customers/jone.png';
|
||||
|
||||
interface CustomerData {
|
||||
id: number;
|
||||
name: string;
|
||||
country: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
export const customerList: CustomerData[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Roselle Ehrman',
|
||||
country: 'Brazil',
|
||||
avatar: roselle,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Jone Smith',
|
||||
country: 'Australia',
|
||||
avatar: jone,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Darron Handler',
|
||||
country: 'Pakistan',
|
||||
avatar: darron,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Leatrice Kulik',
|
||||
country: 'Mascow',
|
||||
avatar: leatrice,
|
||||
},
|
||||
];
|
||||
111
ditch-the-agent/src/data/nav-items.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
export interface NavItem {
|
||||
title: string;
|
||||
path: string;
|
||||
icon?: string;
|
||||
active: boolean;
|
||||
collapsible: boolean;
|
||||
sublist?: NavItem[];
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{
|
||||
title: 'Home',
|
||||
path: '/',
|
||||
icon: 'ion:home-sharp',
|
||||
active: true,
|
||||
collapsible: false,
|
||||
sublist: [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
path: '/',
|
||||
active: false,
|
||||
collapsible: false,
|
||||
},
|
||||
{
|
||||
title: 'Sales',
|
||||
path: '/',
|
||||
active: false,
|
||||
collapsible: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Authentication',
|
||||
path: 'authentication',
|
||||
icon: 'f7:exclamationmark-shield-fill',
|
||||
active: true,
|
||||
collapsible: true,
|
||||
sublist: [
|
||||
{
|
||||
title: 'Sign In',
|
||||
path: 'login',
|
||||
active: true,
|
||||
collapsible: false,
|
||||
},
|
||||
{
|
||||
title: 'Sign Up',
|
||||
path: 'sign-up',
|
||||
active: true,
|
||||
collapsible: false,
|
||||
},
|
||||
{
|
||||
title: 'Forgot password',
|
||||
path: 'forgot-password',
|
||||
active: true,
|
||||
collapsible: false,
|
||||
},
|
||||
{
|
||||
title: 'Reset password',
|
||||
path: 'reset-password',
|
||||
active: true,
|
||||
collapsible: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Notification',
|
||||
path: '#!',
|
||||
icon: 'zondicons:notifications',
|
||||
active: false,
|
||||
collapsible: false,
|
||||
},
|
||||
{
|
||||
title: 'Calendar',
|
||||
path: '#!',
|
||||
icon: 'ph:calendar',
|
||||
active: false,
|
||||
collapsible: false,
|
||||
},
|
||||
{
|
||||
title: 'Message',
|
||||
path: '#!',
|
||||
icon: 'ph:chat-circle-dots-fill',
|
||||
active: false,
|
||||
collapsible: false,
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Property',
|
||||
path: '/property',
|
||||
icon: 'ph:house-line',
|
||||
active: true,
|
||||
collapsible: false,
|
||||
},
|
||||
{
|
||||
title: 'Education',
|
||||
path: '/education',
|
||||
icon: 'ph:student',
|
||||
active: true,
|
||||
collapsible: false,
|
||||
},
|
||||
{
|
||||
title: 'Vendors',
|
||||
path: '/vendors',
|
||||
icon: 'ph:toolbox',
|
||||
active: true,
|
||||
collapsible: false,
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
export default navItems;
|
||||
141
ditch-the-agent/src/data/products.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import relaxingChair from 'assets/top-selling-products/relaxingChair.jpg';
|
||||
import instaxCamera from 'assets/top-selling-products/instaxCamera.jpg';
|
||||
import nikeV22 from 'assets/top-selling-products/nikeV22.jpg';
|
||||
import laptop from 'assets/top-selling-products/laptop.jpg';
|
||||
import watch from 'assets/top-selling-products/watch.jpg';
|
||||
|
||||
export interface DataRow {
|
||||
id: number;
|
||||
product: {
|
||||
avatar: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
};
|
||||
orders: number;
|
||||
price: number;
|
||||
adsSpent: number;
|
||||
refunds: number;
|
||||
}
|
||||
|
||||
export const rows: DataRow[] = [
|
||||
{
|
||||
id: 1,
|
||||
product: {
|
||||
avatar: nikeV22,
|
||||
title: 'Nike v22',
|
||||
subtitle: 'Running Shoes',
|
||||
},
|
||||
orders: 8000,
|
||||
price: 130,
|
||||
adsSpent: 9.5,
|
||||
refunds: 13,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
product: {
|
||||
avatar: instaxCamera,
|
||||
title: 'Instax Camera',
|
||||
subtitle: 'Portable Camera',
|
||||
},
|
||||
orders: 3000,
|
||||
price: 45,
|
||||
adsSpent: 4.5,
|
||||
refunds: 18,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
product: {
|
||||
avatar: relaxingChair,
|
||||
title: 'Chair ',
|
||||
subtitle: 'Relaxing chair',
|
||||
},
|
||||
orders: 6000,
|
||||
price: 80,
|
||||
adsSpent: 5.8,
|
||||
refunds: -11,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
product: {
|
||||
avatar: laptop,
|
||||
title: 'Laptop',
|
||||
subtitle: 'Macbook pro 13',
|
||||
},
|
||||
orders: 4000,
|
||||
price: 500,
|
||||
adsSpent: 4.7,
|
||||
refunds: 18,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
product: {
|
||||
avatar: watch,
|
||||
title: 'Watch',
|
||||
subtitle: 'Digital watch',
|
||||
},
|
||||
orders: 2000,
|
||||
price: 15,
|
||||
adsSpent: 2.5,
|
||||
refunds: -10,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
product: {
|
||||
avatar: relaxingChair,
|
||||
title: 'Chair',
|
||||
subtitle: 'Relaxing chair',
|
||||
},
|
||||
orders: 6000,
|
||||
price: 80,
|
||||
adsSpent: 5.8,
|
||||
refunds: -11,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
product: {
|
||||
avatar: instaxCamera,
|
||||
title: 'Instax Camera',
|
||||
subtitle: 'Portable Camera',
|
||||
},
|
||||
orders: 3000,
|
||||
price: 45,
|
||||
adsSpent: 4.5,
|
||||
refunds: 18,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
product: {
|
||||
avatar: watch,
|
||||
title: 'Watch',
|
||||
subtitle: 'Digital watch',
|
||||
},
|
||||
orders: 2000,
|
||||
price: 15,
|
||||
adsSpent: 2.5,
|
||||
refunds: -10,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
product: {
|
||||
avatar: nikeV22,
|
||||
title: 'Nike v22',
|
||||
subtitle: 'Running Shoes',
|
||||
},
|
||||
orders: 8000,
|
||||
price: 130,
|
||||
adsSpent: 9.5,
|
||||
refunds: 13,
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
product: {
|
||||
avatar: laptop,
|
||||
title: 'Laptop',
|
||||
subtitle: 'Macbook pro 13',
|
||||
},
|
||||
orders: 4000,
|
||||
price: 500,
|
||||
adsSpent: 4.7,
|
||||
refunds: 18,
|
||||
},
|
||||
];
|
||||
39
ditch-the-agent/src/data/sale-info-data.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import avgRevenue from 'assets/sale-info/avg-revenue.png';
|
||||
import customers from 'assets/sale-info/customers.png';
|
||||
import sales from 'assets/sale-info/sales.png';
|
||||
|
||||
interface SaleInfoData {
|
||||
id: number;
|
||||
image: string;
|
||||
title: string;
|
||||
sales: number;
|
||||
increment: number;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export const saleInfoData: SaleInfoData[] = [
|
||||
{
|
||||
id: 1,
|
||||
image: sales,
|
||||
title: 'Sales',
|
||||
sales: 230220,
|
||||
increment: 55,
|
||||
date: 'May 2022',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
image: customers,
|
||||
title: 'Customers',
|
||||
sales: 3200,
|
||||
increment: 12,
|
||||
date: 'May 2022',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
image: avgRevenue,
|
||||
title: 'Avg Revenue',
|
||||
sales: 2300,
|
||||
increment: 210,
|
||||
date: 'May 2022',
|
||||
},
|
||||
];
|
||||
12
ditch-the-agent/src/helpers/capitalize-pathname.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
function capitalizePathname(input: string): string {
|
||||
const lastSegment = input.split('/').at(-1);
|
||||
|
||||
if (lastSegment) {
|
||||
return lastSegment
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
} else return '';
|
||||
}
|
||||
|
||||
export default capitalizePathname;
|
||||
11
ditch-the-agent/src/helpers/format-functions.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const currencyFormat = (amount: number, options: Intl.NumberFormatOptions = {}) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'usd',
|
||||
maximumFractionDigits: 3,
|
||||
minimumFractionDigits: 0,
|
||||
useGrouping: true,
|
||||
notation: 'standard',
|
||||
...options,
|
||||
}).format(amount);
|
||||
};
|
||||
5
ditch-the-agent/src/index.css
Normal file
@@ -0,0 +1,5 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;700&family=Poppins:wght@400;500;600;700&display=swap');
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
19
ditch-the-agent/src/layouts/auth-layout/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { PropsWithChildren, ReactElement } from 'react';
|
||||
import { Stack } from '@mui/material';
|
||||
|
||||
const AuthLayout = ({ children }: PropsWithChildren): ReactElement => {
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
minHeight="100vh"
|
||||
bgcolor="background.default"
|
||||
py={10}
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthLayout;
|
||||
26
ditch-the-agent/src/layouts/main-layout/Footer.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Link, Stack, Typography } from '@mui/material';
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent={{ xs: 'center', md: 'flex-end' }}
|
||||
ml={{ xs: 3.75, lg: 34.75 }}
|
||||
mr={3.75}
|
||||
my={3.75}
|
||||
>
|
||||
<Typography variant="subtitle2" fontFamily={'Poppins'} color="text.primary">
|
||||
<Link
|
||||
href="https://ditchtheagent.com/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
sx={{ color: 'text.primary', '&:hover': { color: 'primary.main' } }}
|
||||
>
|
||||
Ditch The Agent
|
||||
</Link>
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
149
ditch-the-agent/src/layouts/main-layout/Sidebar/NavButton.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import { ReactElement, useState } from 'react';
|
||||
import {
|
||||
Collapse,
|
||||
LinkTypeMap,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
} from '@mui/material';
|
||||
import { OverridableComponent } from '@mui/material/OverridableComponent';
|
||||
import IconifyIcon from 'components/base/IconifyIcon';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { NavItem } from 'data/nav-items';
|
||||
|
||||
interface NavItemProps {
|
||||
navItem: NavItem;
|
||||
Link: OverridableComponent<LinkTypeMap>;
|
||||
}
|
||||
|
||||
const NavButton = ({ navItem, Link }: NavItemProps): ReactElement => {
|
||||
const { pathname } = useLocation();
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [nestedChecked, setNestedChecked] = useState<boolean[]>([]);
|
||||
|
||||
const handleNestedChecked = (index: any, value: boolean) => {
|
||||
const updatedBooleanArray = [...nestedChecked];
|
||||
updatedBooleanArray[index] = value;
|
||||
setNestedChecked(updatedBooleanArray);
|
||||
};
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
sx={{
|
||||
my: 1.25,
|
||||
borderRadius: 2,
|
||||
backgroundColor: pathname === navItem.path ? 'primary.main' : '',
|
||||
color: pathname === navItem.path ? 'common.white' : 'text.secondary',
|
||||
'&:hover': {
|
||||
backgroundColor: pathname === navItem.path ? 'primary.main' : 'action.focus',
|
||||
opacity: 1.5,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{navItem.collapsible ? (
|
||||
<>
|
||||
<ListItemButton LinkComponent={Link} onClick={() => setChecked(!checked)}>
|
||||
<ListItemIcon>
|
||||
<IconifyIcon icon={navItem.icon as string} width={1} height={1} />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{navItem.title}</ListItemText>
|
||||
<ListItemIcon>
|
||||
{navItem.collapsible &&
|
||||
(checked ? (
|
||||
<IconifyIcon icon="mingcute:up-fill" width={1} height={1} />
|
||||
) : (
|
||||
<IconifyIcon icon="mingcute:down-fill" width={1} height={1} />
|
||||
))}
|
||||
</ListItemIcon>
|
||||
</ListItemButton>
|
||||
<Collapse in={checked}>
|
||||
<List>
|
||||
{navItem.sublist?.map((subListItem: any, idx: number) => (
|
||||
<ListItem
|
||||
key={idx}
|
||||
sx={{
|
||||
backgroundColor: pathname === navItem.path ? 'primary.main' : '',
|
||||
color: pathname === navItem.path ? 'common.white' : 'text.secondary',
|
||||
'&:hover': {
|
||||
backgroundColor: pathname === navItem.path ? 'primary.main' : 'action.focus',
|
||||
opacity: 1.5,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{subListItem.collapsible ? (
|
||||
<>
|
||||
<ListItemButton
|
||||
LinkComponent={Link}
|
||||
onClick={() => {
|
||||
handleNestedChecked(idx, !nestedChecked[idx]);
|
||||
}}
|
||||
>
|
||||
<ListItemText sx={{ ml: 3.5 }}>{subListItem.title}</ListItemText>
|
||||
<ListItemIcon>
|
||||
{subListItem.collapsible &&
|
||||
(nestedChecked[idx] ? (
|
||||
<IconifyIcon icon="mingcute:up-fill" width={1} height={1} />
|
||||
) : (
|
||||
<IconifyIcon icon="mingcute:down-fill" width={1} height={1} />
|
||||
))}
|
||||
</ListItemIcon>
|
||||
</ListItemButton>
|
||||
<Collapse in={nestedChecked[idx]}>
|
||||
<List>
|
||||
{subListItem?.sublist?.map(
|
||||
(nestedSubListItem: any, nestedIdx: number) => (
|
||||
<ListItem key={nestedIdx}>
|
||||
<ListItemButton
|
||||
LinkComponent={Link}
|
||||
href={
|
||||
navItem.path !== '/'
|
||||
? navItem.path +
|
||||
'/' +
|
||||
subListItem.path +
|
||||
'/' +
|
||||
nestedSubListItem.path
|
||||
: nestedSubListItem.path
|
||||
}
|
||||
>
|
||||
<ListItemText sx={{ ml: 5 }}>
|
||||
{nestedSubListItem.title}
|
||||
</ListItemText>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
),
|
||||
)}
|
||||
</List>
|
||||
</Collapse>
|
||||
</>
|
||||
) : (
|
||||
<ListItemButton
|
||||
LinkComponent={Link}
|
||||
href={navItem.path + '/' + subListItem.path}
|
||||
>
|
||||
<ListItemText sx={{ ml: 3 }}>{subListItem.title}</ListItemText>
|
||||
</ListItemButton>
|
||||
)}
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Collapse>
|
||||
</>
|
||||
) : (
|
||||
<ListItemButton
|
||||
LinkComponent={Link}
|
||||
href={navItem.path}
|
||||
sx={{ opacity: navItem.active ? 1 : 0.6 }}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<IconifyIcon icon={navItem.icon as string} width={1} height={1} />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{navItem.title}</ListItemText>
|
||||
</ListItemButton>
|
||||
)}
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavButton;
|
||||
109
ditch-the-agent/src/layouts/main-layout/Sidebar/Sidebar.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { ReactElement } from 'react';
|
||||
import {
|
||||
Link,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Stack,
|
||||
} from '@mui/material';
|
||||
|
||||
import IconifyIcon from 'components/base/IconifyIcon';
|
||||
import logo from 'assets/logo/favicon-logo.png';
|
||||
import Image from 'components/base/Image';
|
||||
import navItems from 'data/nav-items';
|
||||
import NavButton from './NavButton';
|
||||
|
||||
const Sidebar = (): ReactElement => {
|
||||
return (
|
||||
<Stack
|
||||
justifyContent="space-between"
|
||||
bgcolor="background.paper"
|
||||
height={1}
|
||||
boxShadow={(theme) => theme.shadows[4]}
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
margin: { xs: 0, lg: 3.75 },
|
||||
borderRadius: { xs: 0, lg: 5 },
|
||||
'&:hover': {
|
||||
overflowY: 'auto',
|
||||
},
|
||||
width: 218,
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href="/"
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
zIndex: 5,
|
||||
mt: 6.25,
|
||||
mx: 4.0625,
|
||||
mb: 3.75,
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 5,
|
||||
}}
|
||||
>
|
||||
<Image src={logo} width={1} />
|
||||
</Link>
|
||||
<Stack
|
||||
justifyContent="space-between"
|
||||
mt={16.25}
|
||||
height={1}
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
'&:hover': {
|
||||
overflowY: 'auto',
|
||||
},
|
||||
width: 218,
|
||||
}}
|
||||
>
|
||||
<List
|
||||
sx={{
|
||||
mx: 2.5,
|
||||
py: 1.25,
|
||||
flex: '1 1 auto',
|
||||
width: 178,
|
||||
}}
|
||||
>
|
||||
{navItems.map((navItem, index) => (
|
||||
<NavButton key={index} navItem={navItem} Link={Link} />
|
||||
))}
|
||||
</List>
|
||||
<List
|
||||
sx={{
|
||||
mx: 2.5,
|
||||
}}
|
||||
>
|
||||
<ListItem
|
||||
sx={{
|
||||
mx: 0,
|
||||
my: 2.5,
|
||||
}}
|
||||
>
|
||||
<ListItemButton
|
||||
LinkComponent={Link}
|
||||
href="/"
|
||||
sx={{
|
||||
backgroundColor: 'background.paper',
|
||||
color: 'primary.main',
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'common.white',
|
||||
opacity: 1.5,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<IconifyIcon icon="ri:logout-circle-line" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Log out</ListItemText>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Divider,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import IconifyIcon from 'components/base/IconifyIcon';
|
||||
import { MouseEvent, ReactElement, useState } from 'react';
|
||||
import profile from 'assets/profile/profile.jpg';
|
||||
|
||||
const AccountDropdown = (): ReactElement => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
color="inherit"
|
||||
id="account-dropdown-button"
|
||||
aria-controls={open ? 'account-dropdown-menu' : undefined}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
onClick={handleClick}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
gap: 1.875,
|
||||
px: { xs: 0, sm: 0.625 },
|
||||
py: 0.625,
|
||||
}}
|
||||
>
|
||||
<Tooltip title="Aiden Max" placement="top" arrow enterDelay={0} leaveDelay={0}>
|
||||
<Avatar alt="Aiden Max" src={profile} sx={{ width: 45, height: 45 }} />
|
||||
</Tooltip>
|
||||
<Typography
|
||||
variant="body1"
|
||||
component="p"
|
||||
color="text.primary"
|
||||
display={{ xs: 'none', sm: 'block' }}
|
||||
>
|
||||
Aiden Max
|
||||
</Typography>
|
||||
<IconifyIcon
|
||||
icon="ion:caret-down-outline"
|
||||
width={24}
|
||||
height={24}
|
||||
color="text.primary"
|
||||
display={{ xs: 'none', sm: 'block' }}
|
||||
/>
|
||||
</Button>
|
||||
<Menu
|
||||
id="account-dropdown-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
MenuListProps={{
|
||||
'aria-labelledby': 'account-dropdown-button',
|
||||
}}
|
||||
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
||||
>
|
||||
<MenuItem onClick={handleClose}>
|
||||
<ListItemIcon>
|
||||
<IconifyIcon icon="ion:home-sharp" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
sx={(theme) => ({
|
||||
'& .MuiListItemText-primary': {
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
fontFamily: theme.typography.body1.fontFamily,
|
||||
fontWeight: theme.typography.body1.fontWeight,
|
||||
},
|
||||
})}
|
||||
>
|
||||
Home
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleClose}>
|
||||
<ListItemIcon>
|
||||
<IconifyIcon icon="mdi:account-outline" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
sx={(theme) => ({
|
||||
'& .MuiListItemText-primary': {
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
fontFamily: theme.typography.body1.fontFamily,
|
||||
fontWeight: theme.typography.body1.fontWeight,
|
||||
},
|
||||
})}
|
||||
>
|
||||
Profile
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleClose}>
|
||||
<ListItemIcon>
|
||||
<IconifyIcon icon="material-symbols:settings" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
sx={(theme) => ({
|
||||
'& .MuiListItemText-primary': {
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
fontFamily: theme.typography.body1.fontFamily,
|
||||
fontWeight: theme.typography.body1.fontWeight,
|
||||
},
|
||||
})}
|
||||
>
|
||||
Settings
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem
|
||||
onClick={handleClose}
|
||||
disableRipple
|
||||
disableTouchRipple
|
||||
sx={{ color: 'error.main' }}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<IconifyIcon icon="ri:logout-circle-line" color="error.main" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
sx={(theme) => ({
|
||||
'& .MuiListItemText-primary': {
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
fontFamily: theme.typography.body1.fontFamily,
|
||||
fontWeight: theme.typography.body1.fontWeight,
|
||||
},
|
||||
})}
|
||||
>
|
||||
Logout
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountDropdown;
|
||||
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
IconButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import IconifyIcon from 'components/base/IconifyIcon';
|
||||
import { MouseEvent, ReactElement, useState } from 'react';
|
||||
|
||||
interface Language {
|
||||
id: number;
|
||||
value: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const languages: Language[] = [
|
||||
{
|
||||
id: 0,
|
||||
value: 'eng',
|
||||
label: 'English',
|
||||
icon: 'twemoji:flag-united-kingdom',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
value: 'fr',
|
||||
label: 'Française',
|
||||
icon: 'twemoji:flag-france',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
value: 'ban',
|
||||
label: 'বাংলা',
|
||||
icon: 'twemoji:flag-bangladesh',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
value: 'zho',
|
||||
label: '官话',
|
||||
icon: 'twemoji:flag-china',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
value: 'hin',
|
||||
label: 'हिन्दी',
|
||||
icon: 'twemoji:flag-india',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
value: 'ara',
|
||||
label: 'Arabic',
|
||||
icon: 'twemoji:flag-saudi-arabia',
|
||||
},
|
||||
];
|
||||
|
||||
const LanguageDropdown = (): ReactElement => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleClickItem = (event: MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleMenuItemClick = (id: number) => {
|
||||
setSelectedIndex(id);
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={handleClickItem}
|
||||
id="language-menu"
|
||||
aria-controls={open ? 'language-menu' : undefined}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
sx={{
|
||||
backgroundColor: 'inherit',
|
||||
borderRadius: 999,
|
||||
paddingLeft: 1,
|
||||
paddingRight: 1,
|
||||
}}
|
||||
>
|
||||
<IconifyIcon icon={languages[selectedIndex].icon} width={24} height={24} />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="language-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
MenuListProps={{
|
||||
'aria-labelledby': 'language-button',
|
||||
}}
|
||||
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
||||
>
|
||||
{languages.map((language) => (
|
||||
<MenuItem
|
||||
key={language.id}
|
||||
selected={language.id === selectedIndex}
|
||||
onClick={() => handleMenuItemClick(language.id)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<IconifyIcon icon={language.icon} />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="subtitle1">{language.label}</Typography>
|
||||
<Typography variant="subtitle2">{language.value}</Typography>
|
||||
</Stack>
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageDropdown;
|
||||
117
ditch-the-agent/src/layouts/main-layout/Topbar/Topbar.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { MouseEventHandler, ReactElement } from 'react';
|
||||
import {
|
||||
AppBar,
|
||||
Badge,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
Link,
|
||||
Stack,
|
||||
TextField,
|
||||
Toolbar,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import IconifyIcon from 'components/base/IconifyIcon';
|
||||
import { drawerWidth } from 'layouts/main-layout';
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import capitalizePathname from 'helpers/capitalize-pathname';
|
||||
import AccountDropdown from './AccountDropdown';
|
||||
import LanguageDropdown from './LanguageDropdown';
|
||||
import Image from 'components/base/Image';
|
||||
import logo from 'assets/logo/favicon-logo.png';
|
||||
|
||||
interface TopbarProps {
|
||||
handleDrawerToggle: MouseEventHandler;
|
||||
}
|
||||
|
||||
const Topbar = ({ handleDrawerToggle }: TopbarProps): ReactElement => {
|
||||
const { pathname } = useLocation();
|
||||
const title = capitalizePathname(pathname);
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
sx={{
|
||||
width: { lg: `calc(100% - ${drawerWidth}px + 24px)` },
|
||||
ml: { lg: `${drawerWidth}px` },
|
||||
}}
|
||||
>
|
||||
<Toolbar
|
||||
sx={{
|
||||
p: 3.75,
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" gap={1}>
|
||||
<Link href="/" width={40} height={40} display={{ xs: 'block', lg: 'none' }}>
|
||||
<IconButton color="inherit" sx={{ p: 0.75, bgcolor: 'inherit' }}>
|
||||
<Image src={logo} width={1} height={1} />
|
||||
</IconButton>
|
||||
</Link>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
edge="start"
|
||||
onClick={handleDrawerToggle}
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
m: 0,
|
||||
p: 0.75,
|
||||
display: { lg: 'none' },
|
||||
bgcolor: 'inherit',
|
||||
}}
|
||||
>
|
||||
<IconifyIcon icon="mdi:menu" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
p: 1,
|
||||
display: { xs: 'flex', lg: 'none' },
|
||||
mr: 'auto',
|
||||
bgcolor: 'inherit',
|
||||
}}
|
||||
>
|
||||
<IconifyIcon icon="mdi:search" width={1} height={1} />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Stack
|
||||
display={{ xs: 'none', lg: 'flex' }}
|
||||
direction="row"
|
||||
gap={{ lg: 6.25 }}
|
||||
alignItems="center"
|
||||
flex={'1 1 auto'}
|
||||
>
|
||||
<Typography variant="h5" component="h5">
|
||||
{pathname === '/' ? 'Dashboard' : title}
|
||||
</Typography>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
placeholder="Search..."
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end" sx={{ width: 24, height: 24 }}>
|
||||
<IconifyIcon icon="mdi:search" width={1} height={1} />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
fullWidth
|
||||
sx={{ maxWidth: 330 }}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row" alignItems="center" gap={{ xs: 1, sm: 1.75 }}>
|
||||
<LanguageDropdown />
|
||||
<IconButton color="inherit" centerRipple sx={{ bgcolor: 'inherit', p: 0.75 }}>
|
||||
<Badge badgeContent={1} color="primary">
|
||||
<IconifyIcon icon="carbon:notification-filled" width={24} height={24} />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
<AccountDropdown />
|
||||
</Stack>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
};
|
||||
|
||||
export default Topbar;
|
||||
93
ditch-the-agent/src/layouts/main-layout/index.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { PropsWithChildren, ReactElement, useState } from 'react';
|
||||
import { Box, Drawer, Stack, Toolbar } from '@mui/material';
|
||||
|
||||
|
||||
|
||||
import Sidebar from 'layouts/main-layout/Sidebar/Sidebar';
|
||||
import Topbar from 'layouts/main-layout/Topbar/Topbar';
|
||||
import Footer from './Footer';
|
||||
import FloatingChatButton from 'components/FloatingChatButton';
|
||||
|
||||
export const drawerWidth = 278;
|
||||
|
||||
const MainLayout = ({ children }: PropsWithChildren): ReactElement => {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
|
||||
const handleDrawerClose = () => {
|
||||
setIsClosing(true);
|
||||
setMobileOpen(false);
|
||||
};
|
||||
|
||||
const handleDrawerTransitionEnd = () => {
|
||||
setIsClosing(false);
|
||||
};
|
||||
|
||||
const handleDrawerToggle = () => {
|
||||
if (!isClosing) {
|
||||
setMobileOpen(!mobileOpen);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" minHeight="100vh" bgcolor="background.default">
|
||||
<Topbar handleDrawerToggle={handleDrawerToggle} />
|
||||
<Box
|
||||
component="nav"
|
||||
sx={{ width: { lg: drawerWidth }, flexShrink: { lg: 0 } }}
|
||||
aria-label="mailbox folders"
|
||||
>
|
||||
<Drawer
|
||||
variant="temporary"
|
||||
open={mobileOpen}
|
||||
onTransitionEnd={handleDrawerTransitionEnd}
|
||||
onClose={handleDrawerClose}
|
||||
ModalProps={{
|
||||
keepMounted: true, // Better open performance on mobile.
|
||||
}}
|
||||
sx={{
|
||||
display: { xs: 'block', lg: 'none' },
|
||||
'& .MuiDrawer-paper': {
|
||||
boxSizing: 'border-box',
|
||||
border: 0,
|
||||
backgroundColor: 'background.default',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Sidebar />
|
||||
</Drawer>
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
sx={{
|
||||
display: { xs: 'none', lg: 'block' },
|
||||
'& .MuiDrawer-paper': {
|
||||
boxSizing: 'border-box',
|
||||
width: drawerWidth,
|
||||
border: 0,
|
||||
backgroundColor: 'background.default',
|
||||
},
|
||||
}}
|
||||
open
|
||||
>
|
||||
<Sidebar />
|
||||
</Drawer>
|
||||
</Box>
|
||||
<Toolbar
|
||||
sx={{
|
||||
pt: 12,
|
||||
width: 1,
|
||||
pb: 0,
|
||||
}}
|
||||
>
|
||||
|
||||
{children}
|
||||
<FloatingChatButton />
|
||||
</Toolbar>
|
||||
</Stack>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainLayout;
|
||||
29
ditch-the-agent/src/main.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { theme } from './theme/theme.ts';
|
||||
import { CssBaseline, ThemeProvider } from '@mui/material';
|
||||
import BreakpointsProvider from 'providers/BreakpointsProvider.tsx';
|
||||
import router from 'routes/router.tsx';
|
||||
import { AuthProvider } from 'contexts/AuthContext.tsx';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
|
||||
|
||||
|
||||
<ThemeProvider theme={theme}>
|
||||
<BreakpointsProvider>
|
||||
<AuthProvider>
|
||||
|
||||
|
||||
<CssBaseline />
|
||||
|
||||
<RouterProvider router={router} />
|
||||
</AuthProvider>
|
||||
</BreakpointsProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
</React.StrictMode>,
|
||||
);
|
||||
29
ditch-the-agent/src/pages/Education/Education.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { drawerWidth } from 'layouts/main-layout';
|
||||
import Grid from '@mui/material/Unstable_Grid2';
|
||||
import { EducationInfoCards } from 'components/sections/dashboard/Home/Education/EducationInfo';
|
||||
|
||||
const Education = (): ReactElement => {
|
||||
return(
|
||||
<Grid
|
||||
container
|
||||
component="main"
|
||||
columns={12}
|
||||
spacing={3.75}
|
||||
flexGrow={1}
|
||||
pt={4.375}
|
||||
pr={1.875}
|
||||
pb={0}
|
||||
sx={{
|
||||
width: { md: `calc(100% - ${drawerWidth}px)` },
|
||||
pl: { xs: 3.75, lg: 0 },
|
||||
}}
|
||||
>
|
||||
<Grid xs={12} md={12}>
|
||||
<EducationInfoCards />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default Education;
|
||||
60
ditch-the-agent/src/pages/Property/Property.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { drawerWidth } from 'layouts/main-layout';
|
||||
import Grid from '@mui/material/Unstable_Grid2';
|
||||
import PropertyDetailsCard from 'components/sections/dashboard/Home/Property/PropertyDetailsCard';
|
||||
import HomePriceEstimate from 'components/sections/dashboard/Home/Property/HomePriceEstimate';
|
||||
import PhotoGalleryCard from 'components/sections/dashboard/Home/Property/PhotoGalleryCard';
|
||||
import MarketStatistics from 'components/sections/dashboard/Home/Property/MarketStatistics';
|
||||
import PropertyListingCard from 'components/sections/dashboard/Home/Property/PropertyListingCard';
|
||||
import LoanDetailsCard from 'components/sections/dashboard/Home/Property/LoanDetailsCard';
|
||||
import PropertyValueGraphCard from 'components/sections/dashboard/Home/Property/PropertyValueGraphCard';
|
||||
|
||||
const Property = (): ReactElement => {
|
||||
return(
|
||||
<Grid
|
||||
container
|
||||
component="main"
|
||||
columns={12}
|
||||
spacing={3.75}
|
||||
flexGrow={1}
|
||||
pt={4.375}
|
||||
pr={1.875}
|
||||
pb={0}
|
||||
sx={{
|
||||
width: { md: `calc(100% - ${drawerWidth}px)` },
|
||||
pl: { xs: 3.75, lg: 0 },
|
||||
}}
|
||||
>
|
||||
<Grid xs={12} md={8}>
|
||||
<PropertyDetailsCard />
|
||||
|
||||
</Grid>
|
||||
<Grid xs={12} md={4}>
|
||||
<HomePriceEstimate />
|
||||
|
||||
</Grid>
|
||||
<Grid xs={12} md={8}>
|
||||
<PhotoGalleryCard />
|
||||
|
||||
</Grid>
|
||||
<Grid xs={12} md={4}>
|
||||
<MarketStatistics />
|
||||
|
||||
</Grid>
|
||||
<Grid xs={12} md={8}>
|
||||
<PropertyValueGraphCard />
|
||||
|
||||
</Grid>
|
||||
<Grid xs={12} md={4}>
|
||||
<LoanDetailsCard />
|
||||
|
||||
</Grid>
|
||||
<Grid xs={12} md={4}>
|
||||
<PropertyListingCard />
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default Property;
|
||||
26
ditch-the-agent/src/pages/Vendors/Vendors.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { drawerWidth } from 'layouts/main-layout';
|
||||
import Grid from '@mui/material/Unstable_Grid2';
|
||||
|
||||
const Vendors = (): ReactElement => {
|
||||
return(
|
||||
<Grid
|
||||
container
|
||||
component="main"
|
||||
columns={12}
|
||||
spacing={3.75}
|
||||
flexGrow={1}
|
||||
pt={4.375}
|
||||
pr={1.875}
|
||||
pb={0}
|
||||
sx={{
|
||||
width: { md: `calc(100% - ${drawerWidth}px)` },
|
||||
pl: { xs: 3.75, lg: 0 },
|
||||
}}
|
||||
>
|
||||
<p>Vendors</p>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default Vendors;
|
||||
82
ditch-the-agent/src/pages/authentication/ForgotPassword.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
InputAdornment,
|
||||
InputLabel,
|
||||
Link,
|
||||
Skeleton,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import Image from 'components/base/Image';
|
||||
import { Suspense } from 'react';
|
||||
import forgotPassword from 'assets/authentication-banners/green.png';
|
||||
import IconifyIcon from 'components/base/IconifyIcon';
|
||||
import logo from 'assets/logo/favicon-logo.png';
|
||||
|
||||
const ForgotPassword = () => {
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
bgcolor="background.paper"
|
||||
boxShadow={(theme) => theme.shadows[3]}
|
||||
height={560}
|
||||
width={{ md: 960 }}
|
||||
>
|
||||
<Stack width={{ md: 0.5 }} m={2.5} gap={10}>
|
||||
<Link href="/" width="fit-content">
|
||||
<Image src={logo} width={82.6} />
|
||||
</Link>
|
||||
<Stack alignItems="center" gap={6.5} width={330} mx="auto">
|
||||
<Typography variant="h3">Forgot Password</Typography>
|
||||
<FormControl variant="standard" fullWidth>
|
||||
<InputLabel shrink htmlFor="new-password">
|
||||
Email
|
||||
</InputLabel>
|
||||
<TextField
|
||||
variant="filled"
|
||||
placeholder="Enter your email"
|
||||
id="email"
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconifyIcon icon="ic:baseline-email" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button variant="contained" fullWidth>
|
||||
Send Password Reset Link
|
||||
</Button>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Back to{' '}
|
||||
<Link
|
||||
href="/authentication/login"
|
||||
underline="hover"
|
||||
fontSize={(theme) => theme.typography.body1.fontSize}
|
||||
>
|
||||
Log in
|
||||
</Link>
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Suspense
|
||||
fallback={
|
||||
<Skeleton variant="rectangular" height={1} width={1} sx={{ bgcolor: 'primary.main' }} />
|
||||
}
|
||||
>
|
||||
<Image
|
||||
src={forgotPassword}
|
||||
sx={{
|
||||
width: 0.5,
|
||||
display: { xs: 'none', md: 'block' },
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForgotPassword;
|
||||
129
ditch-the-agent/src/pages/authentication/Login.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { ReactElement, Suspense, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
InputLabel,
|
||||
Link,
|
||||
Skeleton,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import loginBanner from 'assets/authentication-banners/green.png';
|
||||
import IconifyIcon from 'components/base/IconifyIcon';
|
||||
import logo from 'assets/logo/favicon-logo.png';
|
||||
import Image from 'components/base/Image';
|
||||
|
||||
const Login = (): ReactElement => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleClickShowPassword = () => setShowPassword(!showPassword);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
bgcolor="background.paper"
|
||||
boxShadow={(theme) => theme.shadows[3]}
|
||||
height={560}
|
||||
width={{ md: 960 }}
|
||||
>
|
||||
<Stack width={{ md: 0.5 }} m={2.5} gap={10}>
|
||||
<Link href="/" height="fit-content">
|
||||
<Image src={logo} width={82.6} />
|
||||
</Link>
|
||||
<Stack alignItems="center" gap={2.5} width={330} mx="auto">
|
||||
<Typography variant="h3">Login</Typography>
|
||||
<FormControl variant="standard" fullWidth>
|
||||
<InputLabel shrink htmlFor="email">
|
||||
Email
|
||||
</InputLabel>
|
||||
<TextField
|
||||
variant="filled"
|
||||
placeholder="Enter your email"
|
||||
id="email"
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconifyIcon icon="ic:baseline-email" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl variant="standard" fullWidth>
|
||||
<InputLabel shrink htmlFor="password">
|
||||
Password
|
||||
</InputLabel>
|
||||
<TextField
|
||||
variant="filled"
|
||||
placeholder="********"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
id="password"
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={handleClickShowPassword}
|
||||
edge="end"
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
>
|
||||
{showPassword ? (
|
||||
<IconifyIcon icon="ic:baseline-key-off" />
|
||||
) : (
|
||||
<IconifyIcon icon="ic:baseline-key" />
|
||||
)}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
alignSelf: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<Link href="/authentication/forgot-password" underline="hover">
|
||||
Forget password
|
||||
</Link>
|
||||
</Typography>
|
||||
<Button variant="contained" fullWidth>
|
||||
Log in
|
||||
</Button>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Don't have an account ?{' '}
|
||||
<Link
|
||||
href="/authentication/sign-up"
|
||||
underline="hover"
|
||||
fontSize={(theme) => theme.typography.body1.fontSize}
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Suspense
|
||||
fallback={
|
||||
<Skeleton variant="rectangular" height={1} width={1} sx={{ bgcolor: 'primary.main' }} />
|
||||
}
|
||||
>
|
||||
<Image
|
||||
alt="Login banner"
|
||||
src={loginBanner}
|
||||
sx={{
|
||||
width: 0.5,
|
||||
display: { xs: 'none', md: 'block' },
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||