inital commit

This commit is contained in:
2025-07-12 21:19:35 -05:00
parent c4a6bad95e
commit f05b05420d
152 changed files with 12282 additions and 0 deletions

View 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
View 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?

View File

@@ -0,0 +1,7 @@
.next/
.vscode-test/
out/
dist/
node_modules/
public/
build/

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

View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

View 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

View File

@@ -0,0 +1,5 @@
import { Outlet } from 'react-router-dom';
const App = () => <Outlet />;
export default App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 997 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 866 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View 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);
}
);

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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} />;
}

View File

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

View File

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

View File

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

View 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 }

View 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,
},
];

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

View 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,
},
];

View 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',
},
];

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

View 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);
};

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

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

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

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

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

View File

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

View File

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

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

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

View 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>,
);

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

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

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

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

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

Some files were not shown because too many files have changed in this diff Show More