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;
|
||||||