First Commit
@@ -0,0 +1,28 @@
|
||||
name: npm Build
|
||||
|
||||
# Controls when the action will run.
|
||||
on:
|
||||
# Triggers the workflow on push or pull request events but only for the master branch
|
||||
push:
|
||||
branches: [stage*]
|
||||
|
||||
jobs:
|
||||
SFTP-deploy:
|
||||
name: 🪛 Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 🚚 Get latest code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 22
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: 🔨 Build Vite Admin - npm
|
||||
run: |
|
||||
cd admin
|
||||
cd vite
|
||||
npm i
|
||||
npm run build
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Prod deploy
|
||||
|
||||
# Controls when the action will run.
|
||||
on:
|
||||
# Triggers the workflow on push or pull request events, but only for the main branch
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'uikit/tailwind/**'
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
if_merged:
|
||||
if: github.event.pull_request.merged == true
|
||||
name: 🎉 Prod Deploy
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 🚚 Get latest code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 📂 Deploy to Server
|
||||
uses: easingthemes/ssh-deploy@v4
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
|
||||
# ARGS: "-rltgoDzvO --delete"
|
||||
SOURCE: 'uikit/tailwind/dist/'
|
||||
REMOTE_HOST: 145.79.3.173
|
||||
REMOTE_USER: u965251139
|
||||
REMOTE_PORT: "65002"
|
||||
TARGET: domains/tailwind.saasable.io/public_html/free
|
||||
EXCLUDE: '/node_modules/'
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 PhoenixCoded
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,197 @@
|
||||
# SaasAble Free Tailwind & React Material UI, UI-Kit with Dashboard Template
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/phoenixcoded/saasable-ui/blob/main/LICENSE)
|
||||
|
||||
SaasAble is a complete solution for building SaaS products as well as non-SaaS products. Whether you’re creating a user-friendly front-end or a powerful admin dashboard, SaasAble has all the tools you need. It’s also flexible — perfect not just for SaaS but also for agencies, digital products, CRMs, blogs, AI platforms, project management, learning systems, and more.
|
||||
|
||||
✨ Support us! If you like this theme, click the ⭐ (Top right) and let it shine
|
||||
|
||||

|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Getting Started](#getting-started)
|
||||
- [Download](#download)
|
||||
- [Depoly](#deploy-your-own)
|
||||
- [Why SaasAble?](#why-saasable)
|
||||
- [What's included in Premium Version?](#whats-included-in-premium-version)
|
||||
- [Documentation](#documentation)
|
||||
- [Browser support](#browser-support)
|
||||
- [Technology Stack](#technology-stack)
|
||||
- [SaasAble Figma UI Kit](#saasable-figma-ui-kit)
|
||||
- [More products from PhoenixCoded](#more-products-from-phoenixcoded)
|
||||
- [Issues?](#issues)
|
||||
- [License](#license)
|
||||
- [Contributor](#contributor)
|
||||
- [Follow us](#follow-us)
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Clone from Github
|
||||
|
||||
```
|
||||
git clone https://github.com/phoenixcoded/saasable-ui.git
|
||||
```
|
||||
|
||||
2. Goto `uikit / admin / tailwind` directory and Install Packages
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Run project
|
||||
|
||||
```
|
||||
For React
|
||||
|
||||
npm run dev
|
||||
```
|
||||
|
||||
```
|
||||
For Vite Admin
|
||||
|
||||
npm run start
|
||||
```
|
||||
|
||||
```
|
||||
For Tailwind
|
||||
|
||||
gulp
|
||||
```
|
||||
|
||||
## Download
|
||||
|
||||
| Product | Preview | Download |
|
||||
| -------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Admin Free | [**Live Preview**](https://free.admin.saasable.io/) | [**Download**](https://github.com/phoenixcoded/saasable-ui)</span> |
|
||||
| Admin Pro | [**Live Preview**](https://admin.saasable.io/) | [**Download**](https://mui.com/store/items/saasable-multipurpose-ui-kit-and-dashboard/)</span> |
|
||||
| Ui Kit React Free | [**Live Preview**](https://free.saasable.io/) | [**Download**](https://github.com/phoenixcoded/saasable-ui/)</span> |
|
||||
| Ui Kit React Pro | [**Live Preview**](https://saasable.io/) | [**Download**](https://mui.com/store/items/saasable-multipurpose-ui-kit-and-dashboard/)</span> |
|
||||
| Ui Kit Tailwind Free | [**Live Preview**](https://tailwind.saasable.io/free) | [**Download**](https://github.com/phoenixcoded/saasable-ui)</span> |
|
||||
| Ui Kit Tailwind Pro | [**Live Preview**](https://tailwind.saasable.io/) | [**Download**](https://codedthemes.com/item/saasable-tailwind-ui-kit/)</span> |
|
||||
|
||||
## Deploy your own
|
||||
|
||||
You can deploy your own version of Next.js Sassable to Vercel with one click:
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fhello-world)
|
||||
|
||||
## Why SaasAble?
|
||||
|
||||
<b>React</b>
|
||||
|
||||
The SaasAble theme is a modern, responsive, and highly customizable solution for SaaS applications, dashboards, and admin panels. It comes with pre-built components, dark and light modes, and is optimized for performance and scalability. With regular updates. It saves development time while ensuring a professional and polished UI.
|
||||
|
||||
<b>Tailwind</b>
|
||||
|
||||
The SaasAble theme is a sleek, responsive, and fully customizable solution built with Tailwind CSS and pure HTML. Designed for SaaS applications, dashboards, and admin panels, it offers a lightweight structure with utility-first styling. It includes a collection of pre-built components, supports both dark and light modes, and ensures fast performance and flexibility. With clean markup and regular updates, it helps developers build professional UIs quickly and efficiently—without relying on complex JavaScript frameworks.
|
||||
|
||||
- Modern & Responsive Design
|
||||
- Pre-Built Components & Layouts
|
||||
- Fully Responsive, all modern browser supported
|
||||
- Customizable & Scalable
|
||||
- Dark & Light Modes
|
||||
- Performance Optimized
|
||||
- Integration Ready
|
||||
- Easy Documentation Guide
|
||||
|
||||
## What's included in Premium Version?
|
||||
|
||||
Looking for even more features? Check out SaasAble's [premium version](https://mui.com/store/items/saasable-multipurpose-ui-kit-and-dashboard/), which offers a complete UI solution that includes both Front End UI components (like Hero sections, CTAs, and Footers) and Admin Interface components (such as Roles & Permissions pages, Analytics dashboards, and Billing systems).
|
||||
|
||||
| [SaasAble Ui Kit React Free](https://free.saasable.io/) | [SaasAble Ui Kit React Pro](https://www.saasable.io/) |
|
||||
| ------------------------------------------------------------------------------ | :---------------------------------------------------- |
|
||||
| **1** Demo Landing | **7+** Demo Landing |
|
||||
| **25** Component Blocks | **200+** Component blocks |
|
||||
| - | ✓ Dark/Light Mode 🌓 |
|
||||
| - | ✓ TypeScript version |
|
||||
| - | ✓ Design files (Figma) |
|
||||
| - | ✓ Multiple color options |
|
||||
| - | ✓ RTL |
|
||||
| - | ✓ Supabase authentication |
|
||||
| - | ✓ [More components](https://saasable.io/sections) |
|
||||
| ✓ [MIT License](https://github.com/phoenixcoded/saasable-ui/blob/main/LICENSE) | ✓ [Pro License](https://mui.com/store/license/) |
|
||||
|
||||
| [SaasAble Admin Free](https://free.saasable.io/) | [SaasAble Admin Pro](https://www.saasable.io/) |
|
||||
| ------------------------------------------------------------------------------ | :----------------------------------------------------------------- |
|
||||
| **9** Demo Pages | **45+** Demo Landing |
|
||||
| | ✓ Multi-language |
|
||||
| - | ✓ Dark/Light Mode 🌓 |
|
||||
| - | ✓ TypeScript version |
|
||||
| - | ✓ Design files (Figma) |
|
||||
| - | ✓ Role Base Authentication |
|
||||
| - | ✓ RTL |
|
||||
| - | ✓ Supabase, AWS authentication |
|
||||
| - | ✓ [More components](https://admin.saasable.io/data-display/avatar) |
|
||||
| ✓ [MIT License](https://github.com/phoenixcoded/saasable-ui/blob/main/LICENSE) | ✓ [Pro License](https://mui.com/store/license/) |
|
||||
|
||||
| [SaasAble Ui kit Tailwind Free](https://tailwind.saasable.io/free) | [SaasAble Ui kit Tailwind Pro](https://tailwind.saasable.io) |
|
||||
| ------------------------------------------------------------------------------ | :----------------------------------------------------------------- |
|
||||
| **1** Demo Landing | **7+** Demo Landing |
|
||||
| **25** Component Blocks | **200+** Component blocks |
|
||||
| - | ✓ Dark/Light Mode 🌓 |
|
||||
| - | ✓ Role Base Authentication |
|
||||
| - | ✓ RTL |
|
||||
| - | ✓ [More components](https://tailwind.saasable.io/block/block.html) |
|
||||
| ✓ [MIT License](https://github.com/phoenixcoded/saasable-ui/blob/main/LICENSE) | ✓ [Pro License](https://mui.com/store/license/) |
|
||||
|
||||
## Documentation
|
||||
|
||||
[SaasAble Documentation](https://phoenixcoded.gitbook.io/saasable) helps you with installation, deployment, and troubleshooting.
|
||||
|
||||
[SaasAble Ui Kit Tailwind Documentation](https://phoenixcoded.gitbook.io/saasable-tailwind) helps you with installation, deployment, and troubleshooting.
|
||||
|
||||
## Browser support
|
||||
|
||||
<img src="https://org-public-assets.s3.us-west-2.amazonaws.com/logos/chrome.png" width="45" height="45" > <img src="https://org-public-assets.s3.us-west-2.amazonaws.com/logos/edge.png" width="45" height="45" > <img src="https://org-public-assets.s3.us-west-2.amazonaws.com/logos/safari.png" width="45" height="45" > <img src="https://org-public-assets.s3.us-west-2.amazonaws.com/logos/firefox.png" width="45" height="45" > <img src="https://org-public-assets.s3.us-west-2.amazonaws.com/logos/opera.png" width="45" height="45" >
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- React 19
|
||||
- Next 16
|
||||
- Vite
|
||||
- TypeScript
|
||||
- Material UI v7
|
||||
- JavaScript
|
||||
- Material 3
|
||||
- Figma
|
||||
- Tailwind
|
||||
- npm/yarn package installer
|
||||
|
||||
## SaasAble Figma UI Kit
|
||||
|
||||
| UI KIT | ADMIN |
|
||||
| ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| <img src="https://org-public-assets.s3.us-west-2.amazonaws.com/Banners/Figma_Free_SaasAble.png" width="450" alt="Figma Free"> | <img src="https://org-public-assets.s3.us-west-2.amazonaws.com/Banners/Figma_Free_SaasAdmin.png" width="450" alt="Figma Free"> |
|
||||
| <img src="https://org-public-assets.s3.us-west-2.amazonaws.com/Banners/Figma_Pro_SaasAble.png" width="450" alt="Figma Pro"> | <img src="https://org-public-assets.s3.us-west-2.amazonaws.com/Banners/Figma_Pro_SaasAdmin.png" width="450" alt="Figma Free"> |
|
||||
|
||||
## More products from PhoenixCoded
|
||||
|
||||
| Dashboard | FREE | PRO |
|
||||
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------ |
|
||||
| <img src="https://org-public-assets.s3.us-west-2.amazonaws.com/logos/Light+Able+with+name.png" height="30" style="display:inline-block; vertical-align:middle;"> | - | [**Pro**](https://1.envato.market/EKD9M4)</span> |
|
||||
| <img src="https://org-public-assets.s3.us-west-2.amazonaws.com/logos/Ablepro_with_name.png" height="30" style="display:inline-block; vertical-align:middle;"> | [**Free**](https://github.com/phoenixcoded/able-pro-free-admin-dashboard-template) | [**Pro**](https://1.envato.market/zNkqj6)</span> |
|
||||
|
||||
## Issues
|
||||
|
||||
To report a bug, please submit an [issue](https://github.com/phoenixcoded/saasable-ui/issues) on Github. We will respond as soon as possible to resolve the issue.
|
||||
|
||||
## License
|
||||
|
||||
- Licensed cover under [MIT](https://github.com/phoenixcoded/saasable-ui/blob/main/LICENSE)
|
||||
|
||||
## Contributor
|
||||
|
||||
**PhoenixCoded Team**
|
||||
|
||||
- https://github.com/phoenixcoded
|
||||
|
||||
**Brijesh Dobariya**
|
||||
|
||||
- https://x.com/dobaria_brijesh
|
||||
|
||||
## Follow us
|
||||
|
||||
- [Github](https://github.com/phoenixcoded)
|
||||
- [Phoenixcoded](https://themeforest.net/user/phoenixcoded)
|
||||
@@ -0,0 +1,37 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
.yarn
|
||||
|
||||
# local env files
|
||||
#.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
# next-env.d.ts
|
||||
@@ -0,0 +1,2 @@
|
||||
.next
|
||||
node_modules
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"bracketSpacing": true,
|
||||
"printWidth": 140,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
nodeLinker: node-modules
|
||||
@@ -0,0 +1,146 @@
|
||||
# SaasAble Free React Material UI,Admin Dashboard Template
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/phoenixcoded/saasable-ui/blob/main/LICENSE)
|
||||
|
||||
SaasAble is a complete solution for building SaaS products as well as non-SaaS products. Whether you’re creating a user-friendly front-end or a powerful admin dashboard, SaasAble has all the tools you need. It’s also flexible — perfect not just for SaaS but also for agencies, digital products, CRMs, blogs, AI platforms, project management, learning systems, and more.
|
||||
|
||||
✨ Support us! If you like this theme, click the ⭐ (Top right) and let it shine.
|
||||
|
||||

|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Getting Started](#getting-started)
|
||||
- [Download](#download)
|
||||
- [Why SaasAble?](#why-saasable)
|
||||
- [What's included in Premium Version?](#whats-included-in-premium-version)
|
||||
- [Documentation](#documentation)
|
||||
- [Browser support](#browser-support)
|
||||
- [Technology Stack](#technology-stack)
|
||||
- [SaasAble Figma Admin](#saasable-figma-admin)
|
||||
- [More products from PhoenixCoded](#more-products-from-phoenixcoded)
|
||||
- [Issues?](#issues)
|
||||
- [License](#license)
|
||||
- [Contributor](#contributor)
|
||||
- [Follow us](#follow-us)
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Clone from Github
|
||||
|
||||
```
|
||||
git clone https://github.com/phoenixcoded/saasable-ui.git
|
||||
```
|
||||
|
||||
2. Goto `admin` directory and Install Packages
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Run project
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Download
|
||||
|
||||
| Product | Preview | Download |
|
||||
| -------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Admin Free | [**Live Preview**](https://free.admin.saasable.io/) | [**Download**](https://github.com/phoenixcoded/saasable-ui)</span> |
|
||||
| Admin Pro | [**Live Preview**](https://admin.saasable.io/) | [**Download**](https://mui.com/store/items/saasable-multipurpose-ui-kit-and-dashboard/)</span> |
|
||||
|
||||
|
||||
## Why SaasAble?
|
||||
|
||||
The SaasAble theme is a modern, responsive, and highly customizable solution for SaaS applications, dashboards, and admin panels. It comes with pre-built components, dark and light modes, and is optimized for performance and scalability. With regular updates. It saves development time while ensuring a professional and polished UI.
|
||||
|
||||
- Modern & Responsive Design
|
||||
- Pre-Built Components & Layouts
|
||||
- Fully Responsive, all modern browser supported
|
||||
- Customizable & Scalable
|
||||
- Dark & Light Modes
|
||||
- Performance Optimized
|
||||
- Integration Ready
|
||||
- Easy Documentation Guide
|
||||
|
||||
## What's included in Premium Version?
|
||||
|
||||
Looking for even more features? Check out SaasAble's [premium version](https://mui.com/store/items/saasable-multipurpose-ui-kit-and-dashboard/), which offers a complete UI solution that includes both Front End UI components (like Hero sections, CTAs, and Footers) and Admin Interface components (such as Roles & Permissions pages, Analytics dashboards, and Billing systems).
|
||||
|
||||
| [SaasAble Admin Free](https://github.com/phoenixcoded/saasable-ui/tree/main/admin) | [SaasAble Admin Pro](https://www.saasable.io/) |
|
||||
| ------------------------------------------------------------------------------ | :----------------------------------------------------------------- |
|
||||
| **9** Demo Pages | **45+** Demo Landing |
|
||||
| | ✓ Multi-language |
|
||||
| - | ✓ Dark/Light Mode 🌓 |
|
||||
| - | ✓ TypeScript version |
|
||||
| - | ✓ Design files (Figma) |
|
||||
| - | ✓ Role Base Authentication |
|
||||
| - | ✓ RTL |
|
||||
| - | ✓ Supabase, AWS authentication |
|
||||
| - | ✓ [More components](https://admin.saasable.io/data-display/avatar) |
|
||||
| ✓ [MIT License](https://github.com/phoenixcoded/saasable-ui/blob/main/LICENSE) | ✓ [Pro License](https://mui.com/store/license/) |
|
||||
|
||||
## Documentation
|
||||
|
||||
[SaasAble Documentation](https://phoenixcoded.gitbook.io/saasable/admin) helps you with installation, deployment, and troubleshooting.
|
||||
|
||||
## Browser support
|
||||
|
||||
<img src="https://org-public-assets.s3.us-west-2.amazonaws.com/logos/chrome.png" width="45" height="45" > <img src="https://org-public-assets.s3.us-west-2.amazonaws.com/logos/edge.png" width="45" height="45" > <img src="https://org-public-assets.s3.us-west-2.amazonaws.com/logos/safari.png" width="45" height="45" > <img src="https://org-public-assets.s3.us-west-2.amazonaws.com/logos/firefox.png" width="45" height="45" > <img src="https://org-public-assets.s3.us-west-2.amazonaws.com/logos/opera.png" width="45" height="45" >
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- React 19
|
||||
- Next js
|
||||
- TypeScript
|
||||
- Material UI v7
|
||||
- JavaScript
|
||||
- Material 3
|
||||
- Figma
|
||||
- npm/yarn package installer
|
||||
|
||||
## SaasAble Figma Admin
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
<a href="https://codedthemes.com/item/mantis-free-figma-ui-kit/">
|
||||
<img src="https://org-public-assets.s3.us-west-2.amazonaws.com/Banners/Figma_Free_SaasAdmin.png" width="450" alt="Figma Free">
|
||||
</a>
|
||||
<a href="https://codedthemes.com/item/mantis-figma-ui-kit/">
|
||||
<img src="https://org-public-assets.s3.us-west-2.amazonaws.com/Banners/Figma_Pro_SaasAdmin.png" width="450" alt="Figma Free">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## More products from PhoenixCoded
|
||||
|
||||
| Dashboard | FREE | PRO |
|
||||
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------ |
|
||||
| <img src="https://org-public-assets.s3.us-west-2.amazonaws.com/logos/Light+Able+with+name.png" height="30" style="display:inline-block; vertical-align:middle;"> | - | [**Pro**](https://1.envato.market/EKD9M4)</span> |
|
||||
| <img src="https://org-public-assets.s3.us-west-2.amazonaws.com/logos/Ablepro_with_name.png" height="30" style="display:inline-block; vertical-align:middle;"> | [**Free**](https://github.com/phoenixcoded/able-pro-free-admin-dashboard-template) | [**Pro**](https://1.envato.market/zNkqj6)</span> |
|
||||
|
||||
## Issues
|
||||
|
||||
To report a bug, please submit an [issue](https://github.com/phoenixcoded/saasable-ui/issues) on Github. We will respond as soon as possible to resolve the issue.
|
||||
|
||||
## License
|
||||
|
||||
- Licensed cover under [MIT](https://github.com/phoenixcoded/saasable-ui/blob/main/LICENSE)
|
||||
|
||||
## Contributor
|
||||
|
||||
**PhoenixCoded Team**
|
||||
|
||||
- https://github.com/phoenixcoded
|
||||
|
||||
**Brijesh Dobariya**
|
||||
|
||||
- https://x.com/dobaria_brijesh
|
||||
|
||||
## Follow us
|
||||
|
||||
- [Github](https://github.com/phoenixcoded)
|
||||
- [Phoenixcoded](https://themeforest.net/user/phoenixcoded)
|
||||
@@ -0,0 +1,89 @@
|
||||
import { fixupConfigRules } from '@eslint/compat';
|
||||
import prettier from 'eslint-plugin-prettier';
|
||||
import react from 'eslint-plugin-react';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
||||
import js from '@eslint/js';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { FlatCompat } from '@eslint/eslintrc';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all
|
||||
});
|
||||
|
||||
export default [
|
||||
...fixupConfigRules(compat.extends('prettier')),
|
||||
|
||||
{
|
||||
plugins: {
|
||||
prettier,
|
||||
react,
|
||||
'react-hooks': reactHooks,
|
||||
'jsx-a11y': jsxA11y
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
}
|
||||
},
|
||||
|
||||
rules: {
|
||||
'react/jsx-filename-extension': 'off',
|
||||
'no-param-reassign': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react/require-default-props': 'off',
|
||||
'react/no-array-index-key': 'off',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/jsx-props-no-spreading': 'off',
|
||||
'import/order': 'off',
|
||||
'no-console': 'off',
|
||||
'no-shadow': 'off',
|
||||
'import/no-cycle': 'off',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'jsx-a11y/label-has-associated-control': 'off',
|
||||
'jsx-a11y/no-autofocus': 'off',
|
||||
'react/jsx-uses-react': 'off',
|
||||
'react/jsx-uses-vars': 'error',
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
'no-unused-vars': 'off',
|
||||
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
patterns: ['@mui/*/*/*', '!@mui/material/test-utils/*']
|
||||
}
|
||||
],
|
||||
|
||||
'no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
vars: 'all',
|
||||
args: 'none'
|
||||
}
|
||||
],
|
||||
|
||||
'prettier/prettier': 'warn'
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: ['node_modules/**'],
|
||||
files: ['src/**/*.{js,jsx}']
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.js", "**/*.js", "**/*.jsx", ".next/types/**/*.js"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -0,0 +1,49 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
|
||||
const cspHeader = `
|
||||
default-src 'self';
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' blob: data: https://flagcdn.com;
|
||||
font-src 'self';
|
||||
object-src 'self';
|
||||
base-uri 'self';
|
||||
form-action 'self';
|
||||
frame-ancestors 'self';
|
||||
connect-src 'self' https://cdn.jsdelivr.net;
|
||||
`;
|
||||
|
||||
const nextConfig = {
|
||||
modularizeImports: {
|
||||
'@mui/material': {
|
||||
transform: '@mui/material/{{member}}'
|
||||
},
|
||||
'@mui/lab': {
|
||||
transform: '@mui/lab/{{member}}'
|
||||
}
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'flagcdn.com',
|
||||
pathname: '**'
|
||||
}
|
||||
]
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/(.*)',
|
||||
headers: [
|
||||
{
|
||||
key: 'Content-Security-Policy',
|
||||
value: cspHeader.replace(/\n/g, '')
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "saas-able-react-mui-admin-next-free",
|
||||
"version": "2.1.0",
|
||||
"author": {
|
||||
"email": "phoenixcoded@gmail.com",
|
||||
"name": "phoenixcoded"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"",
|
||||
"lint:fix": "eslint --fix \"src/**/*.{js,jsx,ts,tsx}\"",
|
||||
"prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/cache": "11.14.0",
|
||||
"@emotion/react": "11.14.0",
|
||||
"@emotion/styled": "11.14.1",
|
||||
"@mui/material": "7.3.8",
|
||||
"@mui/material-nextjs": "7.3.8",
|
||||
"@mui/x-charts": "8.27.0",
|
||||
"@mui/x-date-pickers": "8.27.2",
|
||||
"@tabler/icons-react": "3.37.1",
|
||||
"lodash-es": "4.17.23",
|
||||
"next": "16.1.6",
|
||||
"notistack": "3.0.2",
|
||||
"react": "19.2.4",
|
||||
"react-device-detect": "2.2.3",
|
||||
"react-dom": "19.2.4",
|
||||
"react-hook-form": "7.71.2",
|
||||
"simplebar-react": "3.3.2",
|
||||
"stylis": "4.3.6",
|
||||
"stylis-plugin-rtl": "2.1.1",
|
||||
"swr": "2.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "2.0.2",
|
||||
"@eslint/eslintrc": "3.3.4",
|
||||
"@eslint/js": "9.39.3",
|
||||
"eslint": "9.39.3",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-prettier": "5.5.5",
|
||||
"eslint-plugin-react-hooks": "7.0.1",
|
||||
"prettier": "3.8.1"
|
||||
},
|
||||
"packageManager": "yarn@4.12.0"
|
||||
}
|
||||
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 294 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,11 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7126_27179)">
|
||||
<path d="M12.384 8.96798C11.944 6.75252 14.0464 5.45331 14.0464 5.45331C14.0464 5.45331 13.2267 4.26931 11.8507 3.96531C10.4747 3.65918 9.78613 4.03945 9.0992 4.32798C8.4112 4.61385 8.06773 4.61385 8.06773 4.61385C7.0752 4.61385 6.34933 3.56318 4.45653 4.04318C3.15466 4.37171 1.78186 5.87571 1.53386 7.67198C1.28533 9.46771 1.8208 11.7594 2.85226 13.5354C3.8848 15.3125 4.93546 15.9813 5.68053 16C6.4256 16.0186 7.17066 15.4656 8.0688 15.3301C8.96693 15.1989 9.5184 15.657 10.3995 15.905C11.2784 16.153 11.5856 15.9216 12.5952 15.0624C13.6085 14.2032 14.5275 11.7365 14.5275 11.7365C14.5275 11.7365 12.8245 11.1861 12.384 8.96798Z" fill="#E6E0E9"/>
|
||||
<path d="M10.4363 2.53169C11.4112 1.53702 11.08 0.000488281 11.08 0.000488281C11.08 0.000488281 9.71149 0.238888 8.79468 1.19409C7.87682 2.15035 8.02882 3.65755 8.02882 3.65755C8.02882 3.65755 9.46029 3.52529 10.4363 2.53169Z" fill="#E6E0E9"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7126_27179">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,11 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_244_19641)">
|
||||
<path d="M18.5766 13.4512C17.9166 10.128 21.0702 8.17924 21.0702 8.17924C21.0702 8.17924 19.8406 6.40324 17.7766 5.94724C15.7126 5.48804 14.6798 6.05844 13.6494 6.49124C12.6174 6.92004 12.1022 6.92004 12.1022 6.92004C10.6134 6.92004 9.52457 5.34404 6.68537 6.06404C4.73257 6.55684 2.67337 8.81284 2.30137 11.5072C1.92857 14.2008 2.73177 17.6384 4.27897 20.3024C5.82777 22.968 7.40377 23.9712 8.52137 23.9992C9.63897 24.0272 10.7566 23.1976 12.1038 22.9944C13.451 22.7976 14.2782 23.4848 15.5998 23.8568C16.9182 24.2288 17.379 23.8816 18.8934 22.5928C20.4134 21.304 21.7918 17.604 21.7918 17.604C21.7918 17.604 19.2374 16.7784 18.5766 13.4512Z" fill="black"/>
|
||||
<path d="M15.6546 3.7968C17.117 2.3048 16.6202 0 16.6202 0C16.6202 0 14.5674 0.3576 13.1922 1.7904C11.8154 3.2248 12.0434 5.4856 12.0434 5.4856C12.0434 5.4856 14.1906 5.2872 15.6546 3.7968Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_244_19641">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,11 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_244_19642)">
|
||||
<path d="M24 12C24 5.3719 18.6281 0 12 0C5.3719 0 0 5.3719 0 12C0 17.9907 4.3875 22.9547 10.125 23.8547V15.4688H7.0781V12H10.125V9.35625C10.125 6.3492 11.9156 4.6875 14.6578 4.6875C15.9703 4.6875 17.3438 4.92188 17.3438 4.92188V7.875H15.8297C14.3391 7.875 13.875 8.8008 13.875 9.75V12H17.2031L16.6711 15.4688H13.875V23.8547C19.6125 22.9547 24 17.9907 24 12Z" fill="#1877F2"/>
|
||||
<path d="M16.6711 15.4688L17.2031 12H13.875V9.75C13.875 8.8008 14.3391 7.875 15.8297 7.875H17.3438V4.92188C17.3438 4.92188 15.9703 4.6875 14.6578 4.6875C11.9157 4.6875 10.125 6.3492 10.125 9.35625V12H7.07812V15.4688H10.125V23.8547C10.7367 23.9508 11.3625 24 12 24C12.6375 24 13.2633 23.9508 13.875 23.8547V15.4688H16.6711Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_244_19642">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 964 B |
@@ -0,0 +1,13 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_244_19643)">
|
||||
<path d="M1.98633 6.83832C2.71289 5.32425 3.73945 4.04457 5.0332 2.9805C6.78633 1.53207 8.79727 0.660191 11.052 0.393003C13.7004 0.0789407 16.1895 0.580503 18.491 1.95863C19.0629 2.30082 19.5973 2.68988 20.1082 3.11175C20.2348 3.21488 20.2207 3.27582 20.1129 3.37894C19.0676 4.41957 18.0223 5.46019 16.9863 6.51019C16.8645 6.63207 16.7988 6.62269 16.6676 6.52425C13.6301 4.18519 9.20039 4.818 6.93633 7.91175C6.53789 8.4555 6.21445 9.04144 5.98945 9.67894C5.9707 9.73519 5.9332 9.78675 5.90508 9.843C5.30039 9.38363 4.69102 8.92425 4.09102 8.46019C3.38789 7.92113 2.68477 7.38207 1.98633 6.83832Z" fill="#E94335"/>
|
||||
<path d="M5.90586 14.2402C6.10742 14.6855 6.27617 15.1496 6.5293 15.5668C7.59336 17.3105 9.11211 18.4309 11.1137 18.8293C12.9184 19.1902 14.6527 18.9465 16.2699 18.0418C16.3262 18.0137 16.3824 17.9855 16.434 17.9574C16.4621 17.9855 16.4855 18.0184 16.5137 18.0418C17.723 18.9793 18.9371 19.9168 20.1465 20.8543C19.5652 21.4309 18.9043 21.8949 18.2012 22.298C16.1621 23.4605 13.9543 23.9293 11.6293 23.7512C8.73242 23.5262 6.23398 22.3824 4.17148 20.3199C3.2668 19.4152 2.52148 18.3934 1.98242 17.2262C2.48398 16.8418 2.98555 16.4621 3.48711 16.0777C4.29336 15.4637 5.09961 14.8543 5.90586 14.2402Z" fill="#34A853"/>
|
||||
<path d="M20.1507 20.8547C18.9413 19.9172 17.7273 18.9797 16.5179 18.0422C16.4898 18.0188 16.4616 17.9859 16.4382 17.9578C16.8554 17.6344 17.282 17.3203 17.6241 16.9078C18.196 16.2234 18.5757 15.45 18.7726 14.5828C18.796 14.475 18.7773 14.4375 18.6694 14.4422C18.6132 14.4469 18.5616 14.4422 18.5054 14.4422C16.5929 14.4422 14.6757 14.4375 12.7632 14.4469C12.5523 14.4469 12.5054 14.3906 12.5101 14.1891C12.5195 12.8297 12.5195 11.4703 12.5101 10.1109C12.5101 9.9375 12.557 9.89062 12.7304 9.89062C16.2366 9.89531 19.7429 9.89531 23.2538 9.89062C23.4038 9.89062 23.4648 9.92812 23.5023 10.0875C23.7882 11.3766 23.7695 12.6703 23.596 13.9734C23.4554 15.0141 23.2023 16.0219 22.8132 16.9969C22.2366 18.4359 21.3882 19.6922 20.2726 20.7703C20.2304 20.8031 20.1882 20.8266 20.1507 20.8547Z" fill="#4285F3"/>
|
||||
<path d="M5.90536 14.2406C5.09911 14.8547 4.29286 15.4641 3.48661 16.0781C2.98505 16.4578 2.48348 16.8422 1.98192 17.2266C1.59286 16.5047 1.33973 15.7406 1.13348 14.9531C0.739734 13.4297 0.678797 11.8828 0.894422 10.3312C1.06317 9.1125 1.41473 7.94062 1.98192 6.84375C2.68505 7.38281 3.38348 7.92656 4.08661 8.46563C4.6913 8.92969 5.29598 9.38906 5.90067 9.84844C5.79755 10.35 5.65223 10.8422 5.60536 11.3578C5.52098 12.2953 5.61005 13.2094 5.87723 14.1094C5.89598 14.1469 5.90067 14.1937 5.90536 14.2406Z" fill="#FABB06"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_244_19643">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
@@ -0,0 +1 @@
|
||||
/* custom styles */
|
||||
@@ -0,0 +1,3 @@
|
||||
# STYLES
|
||||
|
||||
Third-party module styles
|
||||
@@ -0,0 +1,11 @@
|
||||
// @next
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
// @project
|
||||
const UtilsColors = dynamic(() => import('@/views/components/utils/colors'));
|
||||
|
||||
/*************************** UTILS - COLORS ***************************/
|
||||
|
||||
export default function Utils() {
|
||||
return <UtilsColors />;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// @next
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
// @project
|
||||
const Loader = dynamic(() => import('@/components/Loader'));
|
||||
|
||||
/*************************** LOADING ***************************/
|
||||
|
||||
export default function Loading() {
|
||||
return <Loader />;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// @next
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
// @project
|
||||
const UtilsShadow = dynamic(() => import('@/views/components/utils/shadow'));
|
||||
|
||||
/*************************** UTILS - SHADOW ***************************/
|
||||
|
||||
export default function Utils() {
|
||||
return <UtilsShadow />;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// @next
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
// @project
|
||||
const DataDisplayTypography = dynamic(() => import('@/views/components/utils/typography'));
|
||||
|
||||
/*************************** DATA DISPLAY - TYPOGRAPHY ***************************/
|
||||
|
||||
export default function DataDisplay() {
|
||||
return <DataDisplayTypography />;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// @next
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
// @project
|
||||
const DashboardPage = dynamic(() => import('@/views/admin/dashboard'));
|
||||
|
||||
/*************************** DASHBOARD PAGE ***************************/
|
||||
|
||||
export default function DashboardPages() {
|
||||
return <DashboardPage />;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @next
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
// @project
|
||||
const AdminLayout = dynamic(() => import('@/layouts/AdminLayout'));
|
||||
|
||||
/*************************** LAYOUT - ADMIN ***************************/
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return <AdminLayout>{children}</AdminLayout>;
|
||||
}
|
||||
|
||||
Layout.propTypes = { children: PropTypes.any };
|
||||
@@ -0,0 +1,11 @@
|
||||
// @next
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
// @project
|
||||
const Loader = dynamic(() => import('@/components/Loader'));
|
||||
|
||||
/*************************** LOADING ***************************/
|
||||
|
||||
export default function Loading() {
|
||||
return <Loader />;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// @next
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
// @project
|
||||
const SamplePage = dynamic(() => import('@/views/admin/sample-page'));
|
||||
|
||||
/*************************** SAMPLE PAGE ***************************/
|
||||
|
||||
export default function SamplePages() {
|
||||
return <SamplePage />;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
'use client';
|
||||
|
||||
// @mui
|
||||
import InitColorSchemeScript from '@mui/material/InitColorSchemeScript';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @project
|
||||
import Notistack from '@/components/third-party/Notistack';
|
||||
import { ConfigProvider } from '@/contexts/ConfigContext';
|
||||
import ThemeCustomization from '@/themes';
|
||||
|
||||
/*************************** LAYOUT - CONFIG, THEME ***************************/
|
||||
|
||||
export default function ProviderWrapper({ children }) {
|
||||
return (
|
||||
<>
|
||||
<InitColorSchemeScript attribute="data-color-scheme" defaultMode="light" />
|
||||
<ConfigProvider>
|
||||
<ThemeCustomization>
|
||||
<Notistack>{children}</Notistack>
|
||||
</ThemeCustomization>
|
||||
</ConfigProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ProviderWrapper.propTypes = { children: PropTypes.any };
|
||||
@@ -0,0 +1,14 @@
|
||||
import PropTypes from 'prop-types';
|
||||
// @next
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
// @project
|
||||
const AuthLayout = dynamic(() => import('@/layouts/AuthLayout'));
|
||||
|
||||
/*************************** LAYOUT - AUTH PAGES ***************************/
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return <AuthLayout>{children}</AuthLayout>;
|
||||
}
|
||||
|
||||
Layout.propTypes = { children: PropTypes.any };
|
||||
@@ -0,0 +1,12 @@
|
||||
// @next
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
// @project
|
||||
|
||||
const AuthLogin = dynamic(() => import('@/views/auth/login'));
|
||||
|
||||
/*************************** AUTH - LOGIN ***************************/
|
||||
|
||||
export default function Login() {
|
||||
return <AuthLogin />;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// @next
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
// @project
|
||||
const AuthRegister = dynamic(() => import('@/views/auth/register'));
|
||||
|
||||
/*************************** AUTH - REGISTER ***************************/
|
||||
|
||||
export default function Register() {
|
||||
return <AuthRegister />;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
'use client';
|
||||
|
||||
// @project
|
||||
import Error500Page from '@/components/Error500';
|
||||
|
||||
/*************************** ERROR 500 - DATA ***************************/
|
||||
|
||||
const data = {
|
||||
primaryBtn: { children: 'Back to Home Page' },
|
||||
heading: 'Please try again later or feel free to contact us if the problem persists.'
|
||||
};
|
||||
|
||||
/*************************** ERROR - INTERNAL SERVER ERROR ***************************/
|
||||
|
||||
export default function InternalServerError() {
|
||||
return <Error500Page {...data} />;
|
||||
}
|
||||
|
After Width: | Height: | Size: 244 KiB |
@@ -0,0 +1,5 @@
|
||||
/* simpebar styles */
|
||||
@import 'simplebar-react/dist/simplebar.min.css';
|
||||
|
||||
/* public styles */
|
||||
@import '../../public/assets/styles/index.css';
|
||||
@@ -0,0 +1,42 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @style
|
||||
import './globals.css';
|
||||
|
||||
// @mui
|
||||
import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
|
||||
|
||||
// @project
|
||||
import branding from '@/branding.json';
|
||||
import ProviderWrapper from './ProviderWrapper';
|
||||
|
||||
/*************************** METADATA - MAIN ***************************/
|
||||
|
||||
// Configures the viewport settings for the application.
|
||||
export const viewport = {
|
||||
userScalable: false // Disables user scaling of the viewport.
|
||||
};
|
||||
|
||||
export const metadata = {
|
||||
title: `${branding.brandName} React MUI Dashboard Template`,
|
||||
description: `${branding.brandName} React MUI Dashboard Template`
|
||||
};
|
||||
|
||||
/*************************** LAYOUT - ROOT ***************************/
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<script src="https://fomo.codedthemes.com/pixel/gkPddwTcjNYydfr7v3FcZuToUb9ZAevo" defer data-nscript="lazyOnload"></script>
|
||||
</head>
|
||||
<body>
|
||||
<AppRouterCacheProvider options={{ enableCssLayer: true }}>
|
||||
<ProviderWrapper>{children}</ProviderWrapper>
|
||||
</AppRouterCacheProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
RootLayout.propTypes = { children: PropTypes.any };
|
||||
@@ -0,0 +1,11 @@
|
||||
// @next
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
// @project
|
||||
const Loader = dynamic(() => import('@/components/Loader'));
|
||||
|
||||
/*************************** LOADING ***************************/
|
||||
|
||||
export default function Loading() {
|
||||
return <Loader />;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
'use client';
|
||||
|
||||
// @project
|
||||
import Error404Page from '@/components/Error404';
|
||||
|
||||
/*************************** ERROR 404 - DATA ***************************/
|
||||
|
||||
const data = {
|
||||
primaryBtn: { children: 'Back to Home Page', onClick: () => window.history.back() },
|
||||
heading: 'Looks like youve taken a wrong turn. Lets get you back on track!'
|
||||
};
|
||||
|
||||
/*************************** ERROR - NOT FOUND 404 ***************************/
|
||||
|
||||
export default function notfound() {
|
||||
return <Error404Page {...data} />;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
// @next
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
// @project
|
||||
import { APP_DEFAULT_PATH } from '@/config';
|
||||
|
||||
/*************************** MAIN - DEFAULT PAGE ***************************/
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
router.replace(APP_DEFAULT_PATH);
|
||||
}, [router]);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"brandName": "SaasAble",
|
||||
"company": {
|
||||
"name": "Phoenixcoded",
|
||||
"url": "https://phoenixcoded.authordesk.app/"
|
||||
},
|
||||
"logo": {
|
||||
"main":"",
|
||||
"logoIcon": ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
'use client';
|
||||
|
||||
import { Activity, useEffect, useState } from 'react';
|
||||
|
||||
// @next
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import MuiBreadcrumbs from '@mui/material/Breadcrumbs';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// @project
|
||||
import { APP_DEFAULT_PATH } from '@/config';
|
||||
import menuItems from '@/menu';
|
||||
import { useGetBreadcrumbsMaster } from '@/states/breadcrumbs';
|
||||
import { generateFocusStyle } from '@/utils/generateFocusStyle';
|
||||
|
||||
// @assets
|
||||
import { IconChevronRight } from '@tabler/icons-react';
|
||||
|
||||
// @data
|
||||
const homeBreadcrumb = { title: 'Home', url: APP_DEFAULT_PATH };
|
||||
|
||||
/*************************** BREADCRUMBS ***************************/
|
||||
|
||||
export default function Breadcrumbs() {
|
||||
const theme = useTheme();
|
||||
const location = usePathname();
|
||||
const { breadcrumbsMaster } = useGetBreadcrumbsMaster();
|
||||
|
||||
const [breadcrumbItems, setBreadcrumbItems] = useState([]);
|
||||
const [activeItem, setActiveItem] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
if (breadcrumbsMaster && breadcrumbsMaster.data?.length && breadcrumbsMaster.activePath === location) {
|
||||
dataHandler(breadcrumbsMaster.data);
|
||||
} else {
|
||||
for (const menu of menuItems?.items) {
|
||||
if (menu.type && menu.type === 'group') {
|
||||
const matchedParents = findParentElements(menu.children || [], location);
|
||||
dataHandler(matchedParents || []);
|
||||
if (matchedParents) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [breadcrumbsMaster, location]);
|
||||
|
||||
const dataHandler = (data) => {
|
||||
const active = data.at(-1);
|
||||
const linkItems = data.slice(0, -1);
|
||||
if (active && active.url != homeBreadcrumb.url) {
|
||||
linkItems.unshift(homeBreadcrumb);
|
||||
}
|
||||
setActiveItem(active);
|
||||
setBreadcrumbItems(linkItems);
|
||||
};
|
||||
|
||||
function findParentElements(navItems, targetUrl, parents = []) {
|
||||
for (const item of navItems) {
|
||||
// Add the current item to the parents array
|
||||
const newParents = [...parents, item];
|
||||
|
||||
// Check if the current item matches the target URL
|
||||
if (item.url && targetUrl.includes(item.url)) {
|
||||
return newParents; // Return the array of parent elements
|
||||
}
|
||||
|
||||
// If the item has children, recurse into them
|
||||
if (item.children) {
|
||||
const result = findParentElements(item.children, targetUrl, newParents);
|
||||
if (result) {
|
||||
return result; // Return the result if found in children
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null; // Return null if no match is found
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiBreadcrumbs aria-label="breadcrumb" separator={<IconChevronRight size={16} />}>
|
||||
{breadcrumbItems.length &&
|
||||
breadcrumbItems.map((item, index) => (
|
||||
<Typography
|
||||
{...(item.url && { component: Link, href: item.url })}
|
||||
variant="body2"
|
||||
sx={{
|
||||
p: 0.5,
|
||||
color: 'grey.700',
|
||||
textDecoration: 'none',
|
||||
...(item.url && { cursor: 'pointer', ':hover': { color: 'primary.main' } }),
|
||||
':focus-visible': { outline: 'none', borderRadius: 0.25, ...generateFocusStyle(theme.vars.palette.primary.main) }
|
||||
}}
|
||||
key={index}
|
||||
>
|
||||
{item.title}
|
||||
</Typography>
|
||||
))}
|
||||
<Activity mode={activeItem ? 'visible' : 'hidden'}>
|
||||
<Typography variant="body2" sx={{ p: 0.5 }}>
|
||||
{activeItem?.title}
|
||||
</Typography>
|
||||
</Activity>
|
||||
</MuiBreadcrumbs>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
// @mui
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
/*************************** COMPONENTS WRAPPER ***************************/
|
||||
|
||||
export default function ComponentsWrapper({ children, title }) {
|
||||
return (
|
||||
<Stack sx={{ gap: { xs: 2, sm: 4 } }}>
|
||||
<Stack sx={{ py: 1.25, justifyContent: 'center' }}>
|
||||
<Typography variant="h6">{title}</Typography>
|
||||
</Stack>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
ComponentsWrapper.propTypes = { children: PropTypes.any, title: PropTypes.string };
|
||||
@@ -0,0 +1,186 @@
|
||||
'use client';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardMedia from '@mui/material/CardMedia';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Fade from '@mui/material/Fade';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import List from '@mui/material/List';
|
||||
import ListItemAvatar from '@mui/material/ListItemAvatar';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// @third-party
|
||||
import { Controller } from 'react-hook-form';
|
||||
|
||||
// @project
|
||||
import { contactSchema } from '@/utils/validation-schema/common';
|
||||
|
||||
// @icons
|
||||
import { IconChevronDown, IconHelp } from '@tabler/icons-react';
|
||||
|
||||
// @data
|
||||
import countries from '@/data/countries';
|
||||
|
||||
/*************************** CONTACT ***************************/
|
||||
|
||||
export default function Contact({
|
||||
dialCode,
|
||||
placeholder = 'ex. 9876x xxxxx',
|
||||
helpText,
|
||||
isDisabled = false,
|
||||
isCountryDisabled = false,
|
||||
fullWidth = false,
|
||||
control,
|
||||
isError = false,
|
||||
onCountryChange
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [selectedCountry, setSelectedCountry] = useState(countries[0]);
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
const id = open ? 'dialcode-popper' : undefined;
|
||||
|
||||
const handleClick = (event) => {
|
||||
setAnchorEl(anchorEl ? null : event.currentTarget);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const data = countries.find((item) => item.dialCode === (dialCode || '+1')) || countries[0];
|
||||
setSelectedCountry(data);
|
||||
if (!dialCode && onCountryChange) onCountryChange(data);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dialCode]);
|
||||
|
||||
const countryChange = (country) => {
|
||||
if (onCountryChange) {
|
||||
onCountryChange(country);
|
||||
}
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={'contact'}
|
||||
rules={contactSchema}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<OutlinedInput
|
||||
placeholder={placeholder}
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
error={isError}
|
||||
{...(helpText && {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end" sx={{ '& svg': { cursor: 'default' } }}>
|
||||
<Tooltip title={helpText}>
|
||||
<IconHelp />
|
||||
</Tooltip>
|
||||
</InputAdornment>
|
||||
)
|
||||
})}
|
||||
disabled={isDisabled}
|
||||
aria-describedby="contact-field"
|
||||
slotProps={{ input: { 'aria-label': 'contact' } }}
|
||||
fullWidth={fullWidth}
|
||||
startAdornment={
|
||||
<>
|
||||
<Stack direction="row" sx={{ minHeight: 'inherit', height: 1, gap: 0.5, alignItems: 'center', mr: 0.75 }}>
|
||||
<Button
|
||||
endIcon={<IconChevronDown width={16} height={16} />}
|
||||
disabled={isDisabled || isCountryDisabled}
|
||||
color="secondary"
|
||||
sx={{
|
||||
...theme.typography.body2,
|
||||
height: 'auto',
|
||||
p: 0,
|
||||
borderRadius: 2,
|
||||
minWidth: 40,
|
||||
'&:hover': { bgcolor: 'transparent' },
|
||||
'&:before': { display: 'none' },
|
||||
'& .MuiInputBase-input:focus': { bgcolor: 'transparent' }
|
||||
}}
|
||||
disableRipple
|
||||
aria-describedby={id}
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{selectedCountry.countryCode}
|
||||
</Button>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
</Stack>
|
||||
<Popper
|
||||
placement="bottom-start"
|
||||
id={id}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
transition
|
||||
popperOptions={{ modifiers: [{ name: 'offset', options: { offset: [-10, 11] } }] }}
|
||||
sx={{ zIndex: 1301 }}
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Fade in={open} {...TransitionProps}>
|
||||
<Card elevation={0} sx={{ border: '1px solid', borderColor: theme.vars.palette.divider, borderRadius: 2 }}>
|
||||
<ClickAwayListener onClickAway={() => setAnchorEl(null)}>
|
||||
<Box sx={{ p: 0.5 }}>
|
||||
<List disablePadding>
|
||||
<Box style={{ maxHeight: 320, width: 280, overflow: 'auto' }}>
|
||||
{countries.map((country, index) => (
|
||||
<ListItemButton
|
||||
key={index}
|
||||
sx={{ borderRadius: 2, mb: 0.25 }}
|
||||
selected={country.dialCode === dialCode}
|
||||
onClick={() => countryChange(country)}
|
||||
>
|
||||
<ListItemAvatar sx={{ minWidth: 32 }}>
|
||||
<CardMedia
|
||||
image={`https://flagcdn.com/w20/${country.countryCode.toLowerCase()}.png`}
|
||||
component="img"
|
||||
sx={{ height: 'fit-content', width: 21 }}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={<Typography variant="body2">{country.name}</Typography>} />
|
||||
</ListItemButton>
|
||||
))}
|
||||
</Box>
|
||||
</List>
|
||||
</Box>
|
||||
</ClickAwayListener>
|
||||
</Card>
|
||||
</Fade>
|
||||
)}
|
||||
</Popper>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Contact.propTypes = {
|
||||
dialCode: PropTypes.any,
|
||||
placeholder: PropTypes.string,
|
||||
helpText: PropTypes.any,
|
||||
isDisabled: PropTypes.bool,
|
||||
isCountryDisabled: PropTypes.bool,
|
||||
fullWidth: PropTypes.bool,
|
||||
control: PropTypes.any,
|
||||
isError: PropTypes.bool,
|
||||
onCountryChange: PropTypes.any
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
// @third-party
|
||||
import * as TablerIcons from '@tabler/icons-react';
|
||||
|
||||
/*************************** DYNAMIC - TABLER ICONS ***************************/
|
||||
|
||||
export default function DynamicIcon({ name, size = 24, color = 'black', stroke = 2 }) {
|
||||
// Dynamically get the icon component based on the `name` prop
|
||||
const IconComponent = TablerIcons[name];
|
||||
|
||||
// If the provided `name` does not match any icon in TablerIcons, return null to avoid rendering errors
|
||||
if (!IconComponent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <IconComponent {...{ size, color, stroke }} />;
|
||||
}
|
||||
|
||||
DynamicIcon.propTypes = { name: PropTypes.any, size: PropTypes.number, color: PropTypes.string, stroke: PropTypes.number };
|
||||
@@ -0,0 +1,52 @@
|
||||
'use client';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import Container from '@mui/material/Container';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// @assets
|
||||
import Error404 from '@/images/maintenance/Error404';
|
||||
|
||||
/*************************** ERROR 404 - PAGES ***************************/
|
||||
|
||||
export default function Error404Page({ primaryBtn, heading }) {
|
||||
const theme = useTheme();
|
||||
const upMD = useMediaQuery(theme.breakpoints.up('md'));
|
||||
const upXL = useMediaQuery(theme.breakpoints.up('xl'));
|
||||
const downMD = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
const isDesktop = (upMD || upXL) && !downMD;
|
||||
|
||||
return (
|
||||
<Container {...(isDesktop && { maxWidth: upXL ? 'xl' : 'lg' })} sx={{ ...(downMD && { px: { xs: 2, sm: 4, md: 0 } }) }}>
|
||||
<Stack
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 1,
|
||||
height: '100vh',
|
||||
py: { xs: 4, sm: 5, md: 6 },
|
||||
minHeight: { xs: 450, sm: 600, md: 800 }
|
||||
}}
|
||||
>
|
||||
<Card sx={{ bgcolor: 'grey.100', borderRadius: { xs: 6, sm: 8, md: 10 }, width: 1, height: 1, boxShadow: 'none' }}>
|
||||
<Stack sx={{ justifyContent: 'center', height: 1, gap: { xs: 4, sm: 1 } }}>
|
||||
<Error404 />
|
||||
<Stack sx={{ gap: 2.25, alignItems: 'center', mt: { sm: -5, lg: -6.25 } }}>
|
||||
<Typography sx={{ width: { xs: 210, sm: 300 }, textAlign: 'center' }}>{heading}</Typography>
|
||||
{primaryBtn && <Button variant="contained" size="medium" {...primaryBtn} />}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
Error404Page.propTypes = { primaryBtn: PropTypes.any, heading: PropTypes.string };
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import Container from '@mui/material/Container';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// @assets
|
||||
import Error500 from '@/images/maintenance/Error500';
|
||||
import Error500Server from '@/images/maintenance/Error500Server';
|
||||
|
||||
/*************************** ERROR 500 - PAGES ***************************/
|
||||
|
||||
export default function Error500Page({ primaryBtn, heading }) {
|
||||
const theme = useTheme();
|
||||
const upMD = useMediaQuery(theme.breakpoints.up('md'));
|
||||
const upXL = useMediaQuery(theme.breakpoints.up('xl'));
|
||||
const downMD = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
const isDesktop = (upMD || upXL) && !downMD;
|
||||
|
||||
return (
|
||||
<Container
|
||||
{...(isDesktop && { maxWidth: upXL ? 'xl' : 'lg' })}
|
||||
sx={{
|
||||
...(downMD && { px: { xs: 2, sm: 4, md: 0 } })
|
||||
}}
|
||||
>
|
||||
<Stack sx={{ width: 1, height: '100vh', py: { xs: 4, sm: 5, md: 6 }, minHeight: { xs: 450, sm: 600, md: 800 } }}>
|
||||
<Card
|
||||
sx={{ bgcolor: 'grey.100', borderRadius: { xs: 6, sm: 8, md: 10 }, width: 1, height: 1, position: 'relative', boxShadow: 'none' }}
|
||||
>
|
||||
<Stack sx={{ alignItems: 'center', justifyContent: 'center', gap: 2.25, height: '70%' }}>
|
||||
<Box sx={{ width: 1, maxWidth: { xs: 340, sm: 486, md: 728 }, p: 2 }}>
|
||||
<Error500 />
|
||||
</Box>
|
||||
<Typography sx={{ textAlign: 'center', width: { xs: 248, sm: 340, md: 448 } }}>{heading}</Typography>
|
||||
{primaryBtn && <Button variant="contained" size="medium" {...primaryBtn} sx={{ zIndex: 1 }} />}
|
||||
</Stack>
|
||||
<Box sx={{ width: { xs: '95%', md: '90%' }, position: 'absolute', left: -2, bottom: -6 }}>
|
||||
<Error500Server />
|
||||
</Box>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
Error500Page.propTypes = { primaryBtn: PropTypes.any, heading: PropTypes.string };
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { Activity, useEffect, useState } from 'react';
|
||||
|
||||
// @mui
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
/*************************** LOADER ***************************/
|
||||
|
||||
export default function Loader() {
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Activity mode={isClient ? 'visible' : 'hidden'}>
|
||||
<Box sx={{ position: 'fixed', top: 0, left: 0, zIndex: 2001, width: '100%' }}>
|
||||
<LinearProgress variant="indeterminate" color="primary" />
|
||||
</Box>
|
||||
</Activity>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
'use client';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @mui
|
||||
import Card from '@mui/material/Card';
|
||||
|
||||
/*************************** MAIN CARD ***************************/
|
||||
|
||||
export default function MainCard({ children, sx = {}, ref, ...others }) {
|
||||
const defaultSx = (theme) => ({
|
||||
p: { xs: 1.75, sm: 2.25, md: 3 },
|
||||
border: `1px solid ${theme.vars.palette.divider}`,
|
||||
borderRadius: 4,
|
||||
boxShadow: theme.vars.customShadows.section
|
||||
});
|
||||
|
||||
const combinedSx = (theme) => ({
|
||||
...defaultSx(theme),
|
||||
...(typeof sx === 'function' ? sx(theme) : sx)
|
||||
});
|
||||
|
||||
return (
|
||||
<Card ref={ref} elevation={0} sx={combinedSx} {...others}>
|
||||
{children}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
MainCard.propTypes = { children: PropTypes.any, sx: PropTypes.object, ref: PropTypes.any, others: PropTypes.any };
|
||||
@@ -0,0 +1,76 @@
|
||||
'use client';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { isValidElement } from 'react';
|
||||
|
||||
// @mui
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Badge from '@mui/material/Badge';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// @project
|
||||
import { AvatarSize } from '@/enum';
|
||||
|
||||
/*************************** NOTIFICATION - LIST ***************************/
|
||||
|
||||
export default function NotificationItem({ avatar, badgeAvatar, title, subTitle, dateTime, isSeen = false }) {
|
||||
const ellipsis = { textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap' };
|
||||
|
||||
const avatarContent = isValidElement(avatar) ? <Avatar color="default">{avatar}</Avatar> : <Avatar {...avatar} />;
|
||||
|
||||
return (
|
||||
<Stack direction="row" sx={{ width: 1, alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
|
||||
<Stack direction="row" sx={{ alignItems: 'center', gap: 1.25, flexShrink: 0 }}>
|
||||
{badgeAvatar ? (
|
||||
// Box component for badge position due to parent Stack component
|
||||
<Box>
|
||||
<Badge
|
||||
overlap="circular"
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
badgeContent={
|
||||
<Avatar
|
||||
color="default"
|
||||
size={AvatarSize.BADGE}
|
||||
sx={{ border: `1px solid`, borderColor: 'common.white' }}
|
||||
{...badgeAvatar}
|
||||
/>
|
||||
}
|
||||
slotProps={{ badge: { sx: { bottom: '22%' } } }}
|
||||
>
|
||||
{avatarContent}
|
||||
</Badge>
|
||||
</Box>
|
||||
) : (
|
||||
avatarContent
|
||||
)}
|
||||
</Stack>
|
||||
{/* minWidth: 0 -> Critical to ensure ellipsis works */}
|
||||
<Stack sx={{ flexGrow: 1, minWidth: 0, maxWidth: 1, gap: 0.25 }}>
|
||||
<Typography variant={isSeen ? 'body2' : 'subtitle2'} {...(isSeen && { color: 'grey.700' })} noWrap sx={ellipsis}>
|
||||
{title}
|
||||
</Typography>
|
||||
{subTitle && (
|
||||
<Typography variant="caption" color="text.secondary" noWrap sx={ellipsis}>
|
||||
{subTitle}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
{dateTime && (
|
||||
<Typography variant="caption" sx={{ marginLeft: 'auto', flexShrink: 0 }} {...(isSeen && { color: 'grey.700' })}>
|
||||
{dateTime}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
NotificationItem.propTypes = {
|
||||
avatar: PropTypes.any,
|
||||
badgeAvatar: PropTypes.any,
|
||||
title: PropTypes.any,
|
||||
subTitle: PropTypes.any,
|
||||
dateTime: PropTypes.any,
|
||||
isSeen: PropTypes.bool
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import PropTypes from 'prop-types';
|
||||
// @mui
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// @icons
|
||||
import { IconPhoto } from '@tabler/icons-react';
|
||||
|
||||
/*************************** PROFILE ***************************/
|
||||
|
||||
export default function Profile({ avatar, title, caption, label, sx, titleProps, captionProps, placeholderIfEmpty }) {
|
||||
return (
|
||||
<Stack direction="row" sx={{ alignItems: 'center', justifyContent: 'space-between', gap: 0.75, width: 'fit-content', ...sx }}>
|
||||
{(avatar?.src || placeholderIfEmpty) && (
|
||||
<Avatar
|
||||
{...avatar}
|
||||
alt="profile"
|
||||
sx={{ ...avatar?.sx, ...(placeholderIfEmpty && { fontSize: 20, '& svg': { width: 26, height: 26 } }) }}
|
||||
>
|
||||
{!avatar?.src && placeholderIfEmpty && <IconPhoto stroke={1} />}
|
||||
</Avatar>
|
||||
)}
|
||||
<Stack sx={{ gap: 0.25 }}>
|
||||
<Stack direction="row" sx={{ alignItems: 'center', gap: 0.5 }}>
|
||||
<Typography variant="subtitle2" {...titleProps} sx={{ color: 'text.primary', whiteSpace: 'nowrap', ...titleProps?.sx }}>
|
||||
{title || (placeholderIfEmpty && 'N/A')}
|
||||
</Typography>
|
||||
{label}
|
||||
</Stack>
|
||||
<Typography variant="caption" {...captionProps} sx={{ color: 'grey.700', ...captionProps?.sx }}>
|
||||
{caption || (placeholderIfEmpty && '---')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
Profile.propTypes = {
|
||||
avatar: PropTypes.any,
|
||||
title: PropTypes.any,
|
||||
caption: PropTypes.any,
|
||||
label: PropTypes.any,
|
||||
sx: PropTypes.any,
|
||||
titleProps: PropTypes.any,
|
||||
captionProps: PropTypes.any,
|
||||
placeholderIfEmpty: PropTypes.any
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @mui
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// @project
|
||||
import MainCard from '@/components/MainCard';
|
||||
|
||||
/*************************** CARD - OVERVIEW ***************************/
|
||||
|
||||
export default function OverviewCard({ title, value, chip, compare, cardProps }) {
|
||||
const chipDefaultProps = { color: 'success', variant: 'text', size: 'small' };
|
||||
|
||||
return (
|
||||
<MainCard {...cardProps}>
|
||||
<Stack sx={{ gap: { xs: 3, md: 4 } }}>
|
||||
<Typography variant="subtitle1">{title}</Typography>
|
||||
<Stack sx={{ gap: 0.5 }}>
|
||||
<Stack direction="row" sx={{ gap: 1, alignItems: 'center' }}>
|
||||
<Typography variant="h4">{value}</Typography>
|
||||
<Chip {...{ ...chipDefaultProps, ...chip }} />
|
||||
</Stack>
|
||||
<Typography variant="caption" color="grey.700">
|
||||
{compare}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</MainCard>
|
||||
);
|
||||
}
|
||||
|
||||
OverviewCard.propTypes = {
|
||||
title: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
chip: PropTypes.any,
|
||||
compare: PropTypes.string,
|
||||
cardProps: PropTypes.any
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @mui
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// @project
|
||||
import MainCard from '@/components/MainCard';
|
||||
|
||||
/*************************** PRESENTATION CARD ***************************/
|
||||
|
||||
export default function PresentationCard({ title, children }) {
|
||||
return (
|
||||
<MainCard>
|
||||
<Stack sx={{ gap: 3.25 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 400 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
{children}
|
||||
</Stack>
|
||||
</MainCard>
|
||||
);
|
||||
}
|
||||
|
||||
PresentationCard.propTypes = { title: PropTypes.string, children: PropTypes.any };
|
||||
@@ -0,0 +1,24 @@
|
||||
import PropTypes from 'prop-types';
|
||||
// @mui
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// @project
|
||||
import { LinearProgressType } from '@/enum';
|
||||
|
||||
/*************************** CARD - PROGRESS ***************************/
|
||||
|
||||
export default function ProgressCard({ title, value, progress }) {
|
||||
return (
|
||||
<Stack sx={{ gap: 0.5 }}>
|
||||
<Stack direction="row" sx={{ alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body2">{title}</Typography>
|
||||
<Typography variant="subtitle1">{value}</Typography>
|
||||
</Stack>
|
||||
<LinearProgress variant="determinate" type={LinearProgressType.LIGHT} {...progress} aria-label="progress" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
ProgressCard.propTypes = { title: PropTypes.string, value: PropTypes.string, progress: PropTypes.any };
|
||||
@@ -0,0 +1,19 @@
|
||||
// @mui
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// @images
|
||||
import { ReadingSideDoodle } from '@/images/illustration';
|
||||
|
||||
/*************************** HEADER - EMPTY NOTIFICATION ***************************/
|
||||
|
||||
export default function EmptyNotification() {
|
||||
return (
|
||||
<Stack sx={{ alignItems: 'center', justifyContent: 'center', height: 236, textAlign: 'center', gap: 1, p: 2 }}>
|
||||
<ReadingSideDoodle />
|
||||
<Typography variant="h6" sx={{ fontWeight: 400, maxWidth: 232 }}>
|
||||
Nothing to see here! You're all up to date.
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @mui
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// @assets
|
||||
import { DumpingDoodle } from '@/images/illustration';
|
||||
|
||||
/*************************** HEADER - EMPTY SEARCH ***************************/
|
||||
|
||||
export default function EmptySearch({ props, ref }) {
|
||||
return (
|
||||
<Stack ref={ref} {...props} sx={{ width: 1, alignItems: 'center', justifyContent: 'center', textAlign: 'center', gap: 1.5, p: 1.5 }}>
|
||||
<Box sx={{ width: 230, height: 170 }}>
|
||||
<DumpingDoodle />
|
||||
</Box>
|
||||
<Stack sx={{ gap: 0.5, width: 220 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 400 }}>
|
||||
No search Result
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||
We have searched more than 120 result but didn’t found anything
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
EmptySearch.propTypes = { props: PropTypes.any, ref: PropTypes.object };
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import CardMedia from '@mui/material/CardMedia';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// @project
|
||||
import branding from '@/branding.json';
|
||||
|
||||
/*************************** LOGO - ICON ***************************/
|
||||
|
||||
export default function LogoIcon() {
|
||||
const theme = useTheme();
|
||||
const logoIconPath = branding.logo.logoIcon;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: { xs: 25, sm: 33, md: 40 },
|
||||
height: 1,
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
display: 'block',
|
||||
'& svg': { display: 'block' }
|
||||
}}
|
||||
>
|
||||
{logoIconPath ? (
|
||||
<CardMedia src={logoIconPath} component="img" alt="logo" sx={{ height: 1 }} />
|
||||
) : (
|
||||
<svg viewBox="0 0 37 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M29.0507 0.657088C32.9601 -1.47888 37.5881 1.90736 36.7407 6.28379L31.081 35.5123C31.0417 35.7758 30.9823 36.0375 30.9023 36.2952C30.7256 36.8969 30.4697 37.3981 30.1515 37.802L30.1236 37.8405C28.4079 40.1894 25.1144 40.7015 22.7675 38.9843C21.6036 38.1327 20.8911 36.8926 20.6777 35.5724L20.6789 35.5732C20.0277 33.124 20.9582 26.5495 25.8412 16.0258L27.7227 18.1367L30.214 7.96335C30.3258 7.50659 29.8291 7.14315 29.4282 7.3884L20.4986 12.8509L23.1853 14.0825C18.1195 19.426 11.0662 24.4251 6.06551 24.9519C4.81627 25.0835 3.32109 24.7555 2.15767 23.9042C-0.18924 22.187 -0.700904 18.8907 1.01484 16.5418L1.02814 16.5237L1.0433 16.5032C1.33101 16.0776 1.73015 15.6819 2.24875 15.3311C2.4702 15.1762 2.70184 15.0398 2.9413 14.9222L29.0507 0.657088ZM9.83615 35.6327C11.3428 36.7129 13.4456 36.3571 14.5329 34.8379C15.2554 33.8285 15.7862 30.5405 16.0612 28.4476C16.1668 27.6438 15.3569 27.0632 14.6305 27.4219C12.739 28.3558 9.79931 29.9167 9.07685 30.9261C7.98955 32.4453 8.3295 34.5525 9.83615 35.6327Z"
|
||||
fill={theme.vars.palette.primary.main}
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @next
|
||||
import NextLink from 'next/link';
|
||||
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import ButtonBase from '@mui/material/ButtonBase';
|
||||
|
||||
// @project
|
||||
import LogoMain from './LogoMain';
|
||||
import LogoIcon from './LogoIcon';
|
||||
import { APP_DEFAULT_PATH } from '@/config';
|
||||
import { generateFocusStyle } from '@/utils/generateFocusStyle';
|
||||
|
||||
/*************************** MAIN - LOGO ***************************/
|
||||
|
||||
export default function LogoSection({ isIcon, sx, to }) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<NextLink href={!to ? APP_DEFAULT_PATH : to} passHref>
|
||||
<ButtonBase disableRipple sx={{ ...sx, '&:focus-visible': generateFocusStyle(theme.vars.palette.primary.main) }} aria-label="logo">
|
||||
{isIcon ? <LogoIcon /> : <LogoMain />}
|
||||
</ButtonBase>
|
||||
</NextLink>
|
||||
);
|
||||
}
|
||||
|
||||
LogoSection.propTypes = { isIcon: PropTypes.bool, sx: PropTypes.any, to: PropTypes.string };
|
||||
@@ -0,0 +1 @@
|
||||
export * from './router-link';
|
||||
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* Client-only wrapper for Next.js `Link`.
|
||||
*
|
||||
* Why this file exists:
|
||||
* - Next.js 16 enforces a strict separation between Server and Client Components.
|
||||
* - MUI components (e.g. Button, Link, MenuItem) require `NextLink` to be
|
||||
* imported from a Client Component.
|
||||
* - Importing `next/link` directly inside Server Components causes build
|
||||
* or prerender errors.
|
||||
*
|
||||
* Usage:
|
||||
* ```ts
|
||||
* import { NextLink } from 'components/NextLink';
|
||||
* ```
|
||||
*
|
||||
* Reference:
|
||||
* https://mui.com/material-ui/integrations/nextjs/#next-js-v16-client-component-restriction
|
||||
*/
|
||||
import NextLink from 'next/link';
|
||||
|
||||
export { NextLink };
|
||||
@@ -0,0 +1,114 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @mui
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Fade from '@mui/material/Fade';
|
||||
import Grow from '@mui/material/Grow';
|
||||
import Slide from '@mui/material/Slide';
|
||||
import Zoom from '@mui/material/Zoom';
|
||||
|
||||
// @third-party
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
|
||||
// @project
|
||||
import { useGetSnackbar } from '@/states/snackbar';
|
||||
import Loader from '@/components/Loader';
|
||||
|
||||
// @assets
|
||||
import { IconAlertTriangle, IconBug, IconChecks, IconInfoCircle, IconSpeakerphone } from '@tabler/icons-react';
|
||||
|
||||
// custom styles
|
||||
const StyledSnackbarProvider = styled(SnackbarProvider)(({ theme }) => ({
|
||||
'&.notistack-MuiContent': {
|
||||
color: theme.vars.palette.background.default
|
||||
},
|
||||
'&.notistack-MuiContent-default': {
|
||||
backgroundColor: theme.vars.palette.primary.main
|
||||
},
|
||||
'&.notistack-MuiContent-error': {
|
||||
backgroundColor: theme.vars.palette.error.main
|
||||
},
|
||||
'&.notistack-MuiContent-success': {
|
||||
backgroundColor: theme.vars.palette.success.main
|
||||
},
|
||||
'&.notistack-MuiContent-info': {
|
||||
backgroundColor: theme.vars.palette.info.main
|
||||
},
|
||||
'&.notistack-MuiContent-warning': {
|
||||
backgroundColor: theme.vars.palette.warning.main
|
||||
},
|
||||
'& #notistack-snackbar': {
|
||||
gap: 8
|
||||
}
|
||||
}));
|
||||
|
||||
/*************************** SNACKBAR - ANIMATION ***************************/
|
||||
|
||||
function TransitionSlideLeft(props) {
|
||||
return <Slide {...props} direction="left" />;
|
||||
}
|
||||
|
||||
function TransitionSlideUp(props) {
|
||||
return <Slide {...props} direction="up" />;
|
||||
}
|
||||
|
||||
function TransitionSlideRight(props) {
|
||||
return <Slide {...props} direction="right" />;
|
||||
}
|
||||
|
||||
function TransitionSlideDown(props) {
|
||||
return <Slide {...props} direction="down" />;
|
||||
}
|
||||
|
||||
function GrowTransition(props) {
|
||||
return <Grow {...props} />;
|
||||
}
|
||||
|
||||
function ZoomTransition(props) {
|
||||
return <Zoom {...props} />;
|
||||
}
|
||||
|
||||
const animation = {
|
||||
SlideLeft: TransitionSlideLeft,
|
||||
SlideUp: TransitionSlideUp,
|
||||
SlideRight: TransitionSlideRight,
|
||||
SlideDown: TransitionSlideDown,
|
||||
Grow: GrowTransition,
|
||||
Zoom: ZoomTransition,
|
||||
Fade
|
||||
};
|
||||
|
||||
const iconSX = { fontSize: '1.15rem' };
|
||||
|
||||
/*************************** SNACKBAR - NOTISTACK ***************************/
|
||||
|
||||
export default function Notistack({ children }) {
|
||||
const { snackbar } = useGetSnackbar();
|
||||
|
||||
if (snackbar === undefined) return <Loader />;
|
||||
|
||||
return (
|
||||
<StyledSnackbarProvider
|
||||
maxSnack={snackbar.maxStack}
|
||||
dense={snackbar.dense}
|
||||
anchorOrigin={snackbar.anchorOrigin}
|
||||
TransitionComponent={animation[snackbar.transition]}
|
||||
iconVariant={
|
||||
snackbar.iconVariant === 'useemojis'
|
||||
? {
|
||||
default: <IconSpeakerphone style={iconSX} />,
|
||||
success: <IconChecks style={iconSX} />,
|
||||
error: <IconBug style={iconSX} />,
|
||||
warning: <IconAlertTriangle style={iconSX} />,
|
||||
info: <IconInfoCircle style={iconSX} />
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
hideIconVariant={snackbar.iconVariant === 'hide' ? true : false}
|
||||
>
|
||||
{children}
|
||||
</StyledSnackbarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Notistack.propTypes = { children: PropTypes.node };
|
||||
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @mui
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// @third-party
|
||||
import MainSimpleBar from 'simplebar-react';
|
||||
import { BrowserView, MobileView } from 'react-device-detect';
|
||||
|
||||
// @project
|
||||
import { withAlpha } from '@/utils/colorUtils';
|
||||
|
||||
// root style
|
||||
const RootStyle = styled(BrowserView)({ flexGrow: 1, height: '100%', overflow: 'hidden' });
|
||||
|
||||
// scroll bar wrapper
|
||||
const SimpleBarStyle = styled(MainSimpleBar)(({ theme }) => ({
|
||||
maxHeight: '100%',
|
||||
'& .simplebar-scrollbar': {
|
||||
'&:before': {
|
||||
background: withAlpha(theme.vars.palette.grey[500], 0.48)
|
||||
},
|
||||
'&.simplebar-visible:before': { opacity: 1 }
|
||||
},
|
||||
'& .simplebar-track.simplebar-vertical': { width: 10 },
|
||||
'& .simplebar-track.simplebar-horizontal .simplebar-scrollbar': { height: 6 },
|
||||
'& .simplebar-mask': { zIndex: 'inherit' }
|
||||
}));
|
||||
|
||||
/*************************** SIMPLE SCROLL BAR ***************************/
|
||||
|
||||
export default function SimpleBar({ children, sx, ...other }) {
|
||||
return (
|
||||
<>
|
||||
<RootStyle>
|
||||
<SimpleBarStyle clickOnTrack={false} sx={sx} data-simplebar-direction="ltr" {...other}>
|
||||
{children}
|
||||
</SimpleBarStyle>
|
||||
</RootStyle>
|
||||
<MobileView>
|
||||
<Box sx={{ overflowX: 'auto', ...sx }} {...other}>
|
||||
{children}
|
||||
</Box>
|
||||
</MobileView>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
SimpleBar.propTypes = { children: PropTypes.any, sx: PropTypes.any, other: PropTypes.any };
|
||||
@@ -0,0 +1,24 @@
|
||||
import PropTypes from 'prop-types';
|
||||
// @mui
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
/*************************** CHART - LEGEND ***************************/
|
||||
|
||||
export default function Legend({ items, onToggle }) {
|
||||
return (
|
||||
<Stack direction="row" sx={{ justifyContent: 'flex-end', gap: 1.5 }}>
|
||||
{items.map((item) => (
|
||||
<Stack key={item.id} direction="row" sx={{ alignItems: 'center', gap: 0.5, cursor: 'pointer' }} onClick={() => onToggle(item.id)}>
|
||||
<Box sx={{ width: 15, height: 15, bgcolor: item.visible ? item.color : 'grey.600', borderRadius: '50%' }} />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{item.label}
|
||||
</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
Legend.propTypes = { items: PropTypes.object, onToggle: PropTypes.func };
|
||||
@@ -0,0 +1,57 @@
|
||||
// @next
|
||||
import { Archivo } from 'next/font/google';
|
||||
|
||||
/*************************** THEME CONSTANT ***************************/
|
||||
|
||||
export const APP_DEFAULT_PATH = '/dashboard';
|
||||
|
||||
export const DRAWER_WIDTH = 254;
|
||||
export const MINI_DRAWER_WIDTH = 76 + 1; // 1px - for right-side border
|
||||
|
||||
export const CSS_VAR_PREFIX = '';
|
||||
|
||||
/*************************** THEME ENUM ***************************/
|
||||
|
||||
export let Themes;
|
||||
|
||||
(function (Themes) {
|
||||
Themes['THEME_HOSTING'] = 'hosting';
|
||||
})(Themes || (Themes = {}));
|
||||
|
||||
export let ThemeMode;
|
||||
|
||||
(function (ThemeMode) {
|
||||
ThemeMode['LIGHT'] = 'light';
|
||||
})(ThemeMode || (ThemeMode = {}));
|
||||
|
||||
export let ThemeDirection;
|
||||
|
||||
(function (ThemeDirection) {
|
||||
ThemeDirection['LTR'] = 'ltr';
|
||||
})(ThemeDirection || (ThemeDirection = {}));
|
||||
|
||||
export let ThemeI18n;
|
||||
|
||||
(function (ThemeI18n) {
|
||||
ThemeI18n['EN'] = 'en';
|
||||
ThemeI18n['FR'] = 'fr';
|
||||
ThemeI18n['RO'] = 'ro';
|
||||
ThemeI18n['ZH'] = 'zh';
|
||||
})(ThemeI18n || (ThemeI18n = {}));
|
||||
|
||||
/*************************** CONFIG ***************************/
|
||||
|
||||
const config = {
|
||||
currentTheme: Themes.THEME_HOSTING,
|
||||
themeDirection: ThemeDirection.LTR,
|
||||
miniDrawer: false,
|
||||
i18n: ThemeI18n.EN
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
/*************************** THEME - FONT FAMILY ***************************/
|
||||
|
||||
const fontArchivo = Archivo({ subsets: ['latin'], display: 'swap', weight: ['400', '500', '600', '700'] });
|
||||
|
||||
export const FONT_ARCHIVO = fontArchivo.style.fontFamily;
|
||||
@@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { createContext, useMemo } from 'react';
|
||||
|
||||
// @project
|
||||
import config from '@/config';
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
|
||||
/*************************** CONFIG CONTEXT ***************************/
|
||||
|
||||
export const ConfigContext = createContext(undefined);
|
||||
|
||||
/*************************** CONFIG PROVIDER ***************************/
|
||||
|
||||
export function ConfigProvider({ children }) {
|
||||
const { state, setState, setField, resetState } = useLocalStorage('sass-able-react-mui-admin-next-free', config);
|
||||
|
||||
const memoizedValue = useMemo(() => ({ state, setState, setField, resetState }), [state, setField, setState, resetState]);
|
||||
|
||||
return <ConfigContext.Provider value={memoizedValue}>{children}</ConfigContext.Provider>;
|
||||
}
|
||||
|
||||
ConfigProvider.propTypes = { children: PropTypes.any };
|
||||
@@ -0,0 +1,235 @@
|
||||
/*************************** DATA - COUNTRIES ***************************/
|
||||
|
||||
const countries = [
|
||||
{ countryCode: 'AD', name: 'Andorra', dialCode: '+376' },
|
||||
{ countryCode: 'AE', name: 'United Arab Emirates', dialCode: '+971' },
|
||||
{ countryCode: 'AF', name: 'Afghanistan', dialCode: '+93' },
|
||||
{ countryCode: 'AG', name: 'Antigua and Barbuda', dialCode: '+1-268' },
|
||||
{ countryCode: 'AI', name: 'Anguilla', dialCode: '+1-264' },
|
||||
{ countryCode: 'AL', name: 'Albania', dialCode: '+355' },
|
||||
{ countryCode: 'AM', name: 'Armenia', dialCode: '+374' },
|
||||
{ countryCode: 'AO', name: 'Angola', dialCode: '+244' },
|
||||
{ countryCode: 'AQ', name: 'Antarctica', dialCode: '+672' },
|
||||
{ countryCode: 'AR', name: 'Argentina', dialCode: '+54' },
|
||||
{ countryCode: 'AS', name: 'American Samoa', dialCode: '+1-684' },
|
||||
{ countryCode: 'AT', name: 'Austria', dialCode: '+43' },
|
||||
{ countryCode: 'AU', name: 'Australia', dialCode: '+61', suggested: true },
|
||||
{ countryCode: 'AW', name: 'Aruba', dialCode: '+297' },
|
||||
{ countryCode: 'AZ', name: 'Azerbaijan', dialCode: '+994' },
|
||||
{ countryCode: 'BA', name: 'Bosnia and Herzegovina', dialCode: '+387' },
|
||||
{ countryCode: 'BB', name: 'Barbados', dialCode: '+1-246' },
|
||||
{ countryCode: 'BD', name: 'Bangladesh', dialCode: '+880' },
|
||||
{ countryCode: 'BE', name: 'Belgium', dialCode: '+32' },
|
||||
{ countryCode: 'BF', name: 'Burkina Faso', dialCode: '+226' },
|
||||
{ countryCode: 'BG', name: 'Bulgaria', dialCode: '+359' },
|
||||
{ countryCode: 'BH', name: 'Bahrain', dialCode: '+973' },
|
||||
{ countryCode: 'BI', name: 'Burundi', dialCode: '+257' },
|
||||
{ countryCode: 'BJ', name: 'Benin', dialCode: '+229' },
|
||||
{ countryCode: 'BL', name: 'Saint Barthelemy', dialCode: '+590' },
|
||||
{ countryCode: 'BM', name: 'Bermuda', dialCode: '+1-441' },
|
||||
{ countryCode: 'BN', name: 'Brunei Darussalam', dialCode: '+673' },
|
||||
{ countryCode: 'BO', name: 'Bolivia', dialCode: '+591' },
|
||||
{ countryCode: 'BR', name: 'Brazil', dialCode: '+55' },
|
||||
{ countryCode: 'BS', name: 'Bahamas', dialCode: '+1-242' },
|
||||
{ countryCode: 'BT', name: 'Bhutan', dialCode: '+975' },
|
||||
{ countryCode: 'BW', name: 'Botswana', dialCode: '+267' },
|
||||
{ countryCode: 'BY', name: 'Belarus', dialCode: '+375' },
|
||||
{ countryCode: 'BZ', name: 'Belize', dialCode: '+501' },
|
||||
{ countryCode: 'CD', name: 'Congo', dialCode: '+243' },
|
||||
{ countryCode: 'CF', name: 'Central African Republic', dialCode: '+236' },
|
||||
{ countryCode: 'CG', name: 'Congo', dialCode: '+242' },
|
||||
{ countryCode: 'CH', name: 'Switzerland', dialCode: '+41' },
|
||||
{ countryCode: 'CI', name: "Cote d'Ivoire", dialCode: '+225' },
|
||||
{ countryCode: 'CK', name: 'Cook Islands', dialCode: '+682' },
|
||||
{ countryCode: 'CL', name: 'Chile', dialCode: '+56' },
|
||||
{ countryCode: 'CM', name: 'Cameroon', dialCode: '+237' },
|
||||
{ countryCode: 'CN', name: 'China', dialCode: '+86' },
|
||||
{ countryCode: 'CO', name: 'Colombia', dialCode: '+57' },
|
||||
{ countryCode: 'CR', name: 'Costa Rica', dialCode: '+506' },
|
||||
{ countryCode: 'CU', name: 'Cuba', dialCode: '+53' },
|
||||
{ countryCode: 'CV', name: 'Cape Verde', dialCode: '+238' },
|
||||
{ countryCode: 'CW', name: 'Curacao', dialCode: '+599' },
|
||||
{ countryCode: 'CY', name: 'Cyprus', dialCode: '+357' },
|
||||
{ countryCode: 'CZ', name: 'Czech Republic', dialCode: '+420' },
|
||||
{ countryCode: 'DE', name: 'Germany', dialCode: '+49', suggested: true },
|
||||
{ countryCode: 'DJ', name: 'Djibouti', dialCode: '+253' },
|
||||
{ countryCode: 'DK', name: 'Denmark', dialCode: '+45' },
|
||||
{ countryCode: 'DM', name: 'Dominica', dialCode: '+1-767' },
|
||||
{ countryCode: 'DO', name: 'Dominican Republic', dialCode: '+1-809' },
|
||||
{ countryCode: 'DZ', name: 'Algeria', dialCode: '+213' },
|
||||
{ countryCode: 'EC', name: 'Ecuador', dialCode: '+593' },
|
||||
{ countryCode: 'EE', name: 'Estonia', dialCode: '+372' },
|
||||
{ countryCode: 'EG', name: 'Egypt', dialCode: '+20' },
|
||||
{ countryCode: 'ER', name: 'Eritrea', dialCode: '+291' },
|
||||
{ countryCode: 'ES', name: 'Spain', dialCode: '+34' },
|
||||
{ countryCode: 'ET', name: 'Ethiopia', dialCode: '+251' },
|
||||
{ countryCode: 'FI', name: 'Finland', dialCode: '+358' },
|
||||
{ countryCode: 'FJ', name: 'Fiji', dialCode: '+679' },
|
||||
{ countryCode: 'FK', name: 'Falkland Islands (Malvinas)', dialCode: '+500' },
|
||||
{ countryCode: 'FM', name: 'Micronesia', dialCode: '+691' },
|
||||
{ countryCode: 'FO', name: 'Faroe Islands', dialCode: '+298' },
|
||||
{ countryCode: 'FR', name: 'France', dialCode: '+33', suggested: true },
|
||||
{ countryCode: 'GA', name: 'Gabon', dialCode: '+241' },
|
||||
{ countryCode: 'GB', name: 'United Kingdom', dialCode: '+44' },
|
||||
{ countryCode: 'GD', name: 'Grenada', dialCode: '+1-473' },
|
||||
{ countryCode: 'GE', name: 'Georgia', dialCode: '+995' },
|
||||
{ countryCode: 'GF', name: 'French Guiana', dialCode: '+594' },
|
||||
{ countryCode: 'GH', name: 'Ghana', dialCode: '+233' },
|
||||
{ countryCode: 'GI', name: 'Gibraltar', dialCode: '+350' },
|
||||
{ countryCode: 'GL', name: 'Greenland', dialCode: '+299' },
|
||||
{ countryCode: 'GM', name: 'Gambia', dialCode: '+220' },
|
||||
{ countryCode: 'GN', name: 'Guinea', dialCode: '+224' },
|
||||
{ countryCode: 'GQ', name: 'Equatorial Guinea', dialCode: '+240' },
|
||||
{ countryCode: 'GR', name: 'Greece', dialCode: '+30' },
|
||||
{ countryCode: 'GT', name: 'Guatemala', dialCode: '+502' },
|
||||
{ countryCode: 'GU', name: 'Guam', dialCode: '+1-671' },
|
||||
{ countryCode: 'GW', name: 'Guinea-Bissau', dialCode: '+245' },
|
||||
{ countryCode: 'GY', name: 'Guyana', dialCode: '+592' },
|
||||
{ countryCode: 'HK', name: 'Hong Kong', dialCode: '+852' },
|
||||
{ countryCode: 'HN', name: 'Honduras', dialCode: '+504' },
|
||||
{ countryCode: 'HR', name: 'Croatia', dialCode: '+385' },
|
||||
{ countryCode: 'HT', name: 'Haiti', dialCode: '+509' },
|
||||
{ countryCode: 'HU', name: 'Hungary', dialCode: '3+6' },
|
||||
{ countryCode: 'ID', name: 'Indonesia', dialCode: '+62' },
|
||||
{ countryCode: 'IE', name: 'Ireland', dialCode: '+353' },
|
||||
{ countryCode: 'IL', name: 'Israel', dialCode: '+972' },
|
||||
{ countryCode: 'IN', name: 'India', dialCode: '+91' },
|
||||
{ countryCode: 'IO', name: 'British Indian Ocean Territory', dialCode: '+246' },
|
||||
{ countryCode: 'IQ', name: 'Iraq', dialCode: '+964' },
|
||||
{ countryCode: 'IR', name: 'Iran', dialCode: '+98' },
|
||||
{ countryCode: 'IS', name: 'Iceland', dialCode: '+354' },
|
||||
{ countryCode: 'IT', name: 'Italy', dialCode: '+39' },
|
||||
{ countryCode: 'JM', name: 'Jamaica', dialCode: '+1-876' },
|
||||
{ countryCode: 'JO', name: 'Jordan', dialCode: '+962' },
|
||||
{ countryCode: 'JP', name: 'Japan', dialCode: '+81', suggested: true },
|
||||
{ countryCode: 'KE', name: 'Kenya', dialCode: '+254' },
|
||||
{ countryCode: 'KG', name: 'Kyrgyzstan', dialCode: '+996' },
|
||||
{ countryCode: 'KH', name: 'Cambodia', dialCode: '+855' },
|
||||
{ countryCode: 'KI', name: 'Kiribati', dialCode: '+686' },
|
||||
{ countryCode: 'KM', name: 'Comoros', dialCode: '+269' },
|
||||
{ countryCode: 'KN', name: 'Saint Kitts and Nevis', dialCode: '+1-869' },
|
||||
{ countryCode: 'KP', name: 'Korea', dialCode: '+850' },
|
||||
{ countryCode: 'KR', name: 'Korea', dialCode: '+82' },
|
||||
{ countryCode: 'KW', name: 'Kuwait', dialCode: '+965' },
|
||||
{ countryCode: 'KY', name: 'Cayman Islands', dialCode: '+1-345' },
|
||||
{ countryCode: 'LA', name: "Lao People's Democratic Republic", dialCode: '+856' },
|
||||
{ countryCode: 'LB', name: 'Lebanon', dialCode: '+961' },
|
||||
{ countryCode: 'LC', name: 'Saint Lucia', dialCode: '+1-758' },
|
||||
{ countryCode: 'LI', name: 'Liechtenstein', dialCode: '+423' },
|
||||
{ countryCode: 'LK', name: 'Sri Lanka', dialCode: '+94' },
|
||||
{ countryCode: 'LR', name: 'Liberia', dialCode: '+231' },
|
||||
{ countryCode: 'LS', name: 'Lesotho', dialCode: '+266' },
|
||||
{ countryCode: 'LT', name: 'Lithuania', dialCode: '+370' },
|
||||
{ countryCode: 'LU', name: 'Luxembourg', dialCode: '+352' },
|
||||
{ countryCode: 'LV', name: 'Latvia', dialCode: '+371' },
|
||||
{ countryCode: 'LY', name: 'Libya', dialCode: '+218' },
|
||||
{ countryCode: 'MA', name: 'Morocco', dialCode: '+212' },
|
||||
{ countryCode: 'MC', name: 'Monaco', dialCode: '+377' },
|
||||
{ countryCode: 'MD', name: 'Moldova', dialCode: '+373' },
|
||||
{ countryCode: 'ME', name: 'Montenegro', dialCode: '+382' },
|
||||
{ countryCode: 'MG', name: 'Madagascar', dialCode: '+261' },
|
||||
{ countryCode: 'MH', name: 'Marshall Islands', dialCode: '+692' },
|
||||
{ countryCode: 'MK', name: 'Macedonia', dialCode: '+389' },
|
||||
{ countryCode: 'ML', name: 'Mali', dialCode: '+223' },
|
||||
{ countryCode: 'MM', name: 'Myanmar', dialCode: '+95' },
|
||||
{ countryCode: 'MN', name: 'Mongolia', dialCode: '+976' },
|
||||
{ countryCode: 'MO', name: 'Macao', dialCode: '+853' },
|
||||
{ countryCode: 'MP', name: 'Northern Mariana Islands', dialCode: '+1-670' },
|
||||
{ countryCode: 'MQ', name: 'Martinique', dialCode: '+596' },
|
||||
{ countryCode: 'MR', name: 'Mauritania', dialCode: '+222' },
|
||||
{ countryCode: 'MS', name: 'Montserrat', dialCode: '+1-664' },
|
||||
{ countryCode: 'MT', name: 'Malta', dialCode: '+356' },
|
||||
{ countryCode: 'MU', name: 'Mauritius', dialCode: '+230' },
|
||||
{ countryCode: 'MV', name: 'Maldives', dialCode: '+960' },
|
||||
{ countryCode: 'MW', name: 'Malawi', dialCode: '+265' },
|
||||
{ countryCode: 'MX', name: 'Mexico', dialCode: '+52' },
|
||||
{ countryCode: 'MY', name: 'Malaysia', dialCode: '+60' },
|
||||
{ countryCode: 'MZ', name: 'Mozambique', dialCode: '+258' },
|
||||
{ countryCode: 'NA', name: 'Namibia', dialCode: '+264' },
|
||||
{ countryCode: 'NC', name: 'New Caledonia', dialCode: '+687' },
|
||||
{ countryCode: 'NE', name: 'Niger', dialCode: '+227' },
|
||||
{ countryCode: 'NG', name: 'Nigeria', dialCode: '+234' },
|
||||
{ countryCode: 'NI', name: 'Nicaragua', dialCode: '+505' },
|
||||
{ countryCode: 'NL', name: 'Netherlands', dialCode: '+31' },
|
||||
{ countryCode: 'NO', name: 'Norway', dialCode: '+47' },
|
||||
{ countryCode: 'NP', name: 'Nepal', dialCode: '+977' },
|
||||
{ countryCode: 'NR', name: 'Nauru', dialCode: '+674' },
|
||||
{ countryCode: 'NU', name: 'Niue', dialCode: '+683' },
|
||||
{ countryCode: 'NZ', name: 'New Zealand', dialCode: '+64' },
|
||||
{ countryCode: 'OM', name: 'Oman', dialCode: '+968' },
|
||||
{ countryCode: 'PA', name: 'Panama', dialCode: '+507' },
|
||||
{ countryCode: 'PE', name: 'Peru', dialCode: '+51' },
|
||||
{ countryCode: 'PF', name: 'French Polynesia', dialCode: '+689' },
|
||||
{ countryCode: 'PG', name: 'Papua New Guinea', dialCode: '+675' },
|
||||
{ countryCode: 'PH', name: 'Philippines', dialCode: '+63' },
|
||||
{ countryCode: 'PK', name: 'Pakistan', dialCode: '+92' },
|
||||
{ countryCode: 'PL', name: 'Poland', dialCode: '+48' },
|
||||
{ countryCode: 'PM', name: 'Saint Pierre and Miquelon', dialCode: '+508' },
|
||||
{ countryCode: 'PN', name: 'Pitcairn', dialCode: '+870' },
|
||||
{ countryCode: 'PS', name: 'Palestine', dialCode: '+970' },
|
||||
{ countryCode: 'PT', name: 'Portugal', dialCode: '+351' },
|
||||
{ countryCode: 'PW', name: 'Palau', dialCode: '+680' },
|
||||
{ countryCode: 'PY', name: 'Paraguay', dialCode: '+595' },
|
||||
{ countryCode: 'QA', name: 'Qatar', dialCode: '+974' },
|
||||
{ countryCode: 'RO', name: 'Romania', dialCode: '+40' },
|
||||
{ countryCode: 'RS', name: 'Serbia', dialCode: '+381' },
|
||||
{ countryCode: 'RU', name: 'Russian Federation', dialCode: '+7' },
|
||||
{ countryCode: 'RW', name: 'Rwanda', dialCode: '+250' },
|
||||
{ countryCode: 'SA', name: 'Saudi Arabia', dialCode: '+966' },
|
||||
{ countryCode: 'SB', name: 'Solomon Islands', dialCode: '+677' },
|
||||
{ countryCode: 'SC', name: 'Seychelles', dialCode: '+248' },
|
||||
{ countryCode: 'SD', name: 'Sudan', dialCode: '+249' },
|
||||
{ countryCode: 'SE', name: 'Sweden', dialCode: '+46' },
|
||||
{ countryCode: 'SG', name: 'Singapore', dialCode: '+65' },
|
||||
{ countryCode: 'SH', name: 'Saint Helena', dialCode: '+290' },
|
||||
{ countryCode: 'SI', name: 'Slovenia', dialCode: '+386' },
|
||||
{ countryCode: 'SK', name: 'Slovakia', dialCode: '+421' },
|
||||
{ countryCode: 'SL', name: 'Sierra Leone', dialCode: '+232' },
|
||||
{ countryCode: 'SM', name: 'San Marino', dialCode: '+378' },
|
||||
{ countryCode: 'SN', name: 'Senegal', dialCode: '+221' },
|
||||
{ countryCode: 'SO', name: 'Somalia', dialCode: '+252' },
|
||||
{ countryCode: 'SR', name: 'Suriname', dialCode: '+597' },
|
||||
{ countryCode: 'SS', name: 'South Sudan', dialCode: '+211' },
|
||||
{ countryCode: 'ST', name: 'Sao Tome and Principe', dialCode: '+239' },
|
||||
{ countryCode: 'SV', name: 'El Salvador', dialCode: '+503' },
|
||||
{ countryCode: 'SX', name: 'Sint Maarten (Dutch part)', dialCode: '+1-721' },
|
||||
{ countryCode: 'SY', name: 'Syrian Arab Republic', dialCode: '+963' },
|
||||
{ countryCode: 'SZ', name: 'Swaziland', dialCode: '+268' },
|
||||
{ countryCode: 'TC', name: 'Turks and Caicos Islands', dialCode: '+1-649' },
|
||||
{ countryCode: 'TD', name: 'Chad', dialCode: '+235' },
|
||||
{ countryCode: 'TG', name: 'Togo', dialCode: '+228' },
|
||||
{ countryCode: 'TH', name: 'Thailand', dialCode: '+66' },
|
||||
{ countryCode: 'TJ', name: 'Tajikistan', dialCode: '+992' },
|
||||
{ countryCode: 'TK', name: 'Tokelau', dialCode: '+690' },
|
||||
{ countryCode: 'TL', name: 'Timor-Leste', dialCode: '+670' },
|
||||
{ countryCode: 'TM', name: 'Turkmenistan', dialCode: '+993' },
|
||||
{ countryCode: 'TN', name: 'Tunisia', dialCode: '+216' },
|
||||
{ countryCode: 'TO', name: 'Tonga', dialCode: '+676' },
|
||||
{ countryCode: 'TR', name: 'Turkey', dialCode: '+90' },
|
||||
{ countryCode: 'TT', name: 'Trinidad and Tobago', dialCode: '+1-868' },
|
||||
{ countryCode: 'TV', name: 'Tuvalu', dialCode: '+688' },
|
||||
{ countryCode: 'TW', name: 'Taiwan', dialCode: '+886' },
|
||||
{ countryCode: 'TZ', name: 'United Republic of Tanzania', dialCode: '+255' },
|
||||
{ countryCode: 'UA', name: 'Ukraine', dialCode: '+380' },
|
||||
{ countryCode: 'UG', name: 'Uganda', dialCode: '+256' },
|
||||
{ countryCode: 'US', name: 'United States', dialCode: '+1', suggested: true },
|
||||
{ countryCode: 'UY', name: 'Uruguay', dialCode: '+598' },
|
||||
{ countryCode: 'UZ', name: 'Uzbekistan', dialCode: '+998' },
|
||||
{ countryCode: 'VA', name: 'Holy See (Vatican City State)', dialCode: '+379' },
|
||||
{ countryCode: 'VC', name: 'Saint Vincent and the Grenadines', dialCode: '+1-784' },
|
||||
{ countryCode: 'VE', name: 'Venezuela', dialCode: '+58' },
|
||||
{ countryCode: 'VG', name: 'British Virgin Islands', dialCode: '+1-284' },
|
||||
{ countryCode: 'VI', name: 'US Virgin Islands', dialCode: '+1-340' },
|
||||
{ countryCode: 'VN', name: 'Vietnam', dialCode: '+84' },
|
||||
{ countryCode: 'VU', name: 'Vanuatu', dialCode: '+678' },
|
||||
{ countryCode: 'WF', name: 'Wallis and Futuna', dialCode: '+681' },
|
||||
{ countryCode: 'WS', name: 'Samoa', dialCode: '+685' },
|
||||
{ countryCode: 'XK', name: 'Kosovo', dialCode: '+383' },
|
||||
{ countryCode: 'YE', name: 'Yemen', dialCode: '+967' },
|
||||
{ countryCode: 'YT', name: 'Mayotte', dialCode: '+262' },
|
||||
{ countryCode: 'ZA', name: 'South Africa', dialCode: '+27' },
|
||||
{ countryCode: 'ZM', name: 'Zambia', dialCode: '+260' },
|
||||
{ countryCode: 'ZW', name: 'Zimbabwe', dialCode: '+263' }
|
||||
];
|
||||
|
||||
export default countries;
|
||||
@@ -0,0 +1,50 @@
|
||||
/** Tabs custom props `type` enum */
|
||||
export let TabsType;
|
||||
|
||||
(function (TabsType) {
|
||||
TabsType['SEGMENTED'] = 'segmented';
|
||||
})(TabsType || (TabsType = {}));
|
||||
|
||||
/** LinearProgress custom props `type` enum */
|
||||
export let LinearProgressType;
|
||||
|
||||
(function (LinearProgressType) {
|
||||
LinearProgressType['LIGHT'] = 'light';
|
||||
})(LinearProgressType || (LinearProgressType = {}));
|
||||
|
||||
/** Chip custom props `position` enum */
|
||||
export let ChipIconPosition;
|
||||
|
||||
(function (ChipIconPosition) {
|
||||
ChipIconPosition['RIGHT'] = 'right';
|
||||
})(ChipIconPosition || (ChipIconPosition = {}));
|
||||
|
||||
/** Avatar custom props `size` enum */
|
||||
export let AvatarSize;
|
||||
|
||||
(function (AvatarSize) {
|
||||
AvatarSize['BADGE'] = 'badge';
|
||||
AvatarSize['XXS'] = 'xxs';
|
||||
AvatarSize['XS'] = 'xs';
|
||||
AvatarSize['SM'] = 'sm';
|
||||
AvatarSize['MD'] = 'md';
|
||||
AvatarSize['LG'] = 'lg';
|
||||
AvatarSize['XL'] = 'xl';
|
||||
})(AvatarSize || (AvatarSize = {}));
|
||||
|
||||
/** Chart custom view mode enum */
|
||||
export let ViewMode;
|
||||
|
||||
(function (ViewMode) {
|
||||
ViewMode['DAILY'] = 'Daily';
|
||||
ViewMode['MONTHLY'] = 'Monthly';
|
||||
ViewMode['YEARLY'] = 'Yearly';
|
||||
})(ViewMode || (ViewMode = {}));
|
||||
|
||||
/** Auth social props `type` enum */
|
||||
export let SocialTypes;
|
||||
|
||||
(function (SocialTypes) {
|
||||
SocialTypes['HORIZONTAL'] = 'horizontal';
|
||||
SocialTypes['VERTICAL'] = 'vertical';
|
||||
})(SocialTypes || (SocialTypes = {}));
|
||||
@@ -0,0 +1,25 @@
|
||||
import { use } from 'react';
|
||||
|
||||
// @project
|
||||
import { ConfigContext } from '@/contexts/ConfigContext';
|
||||
|
||||
/*************************** HOOKS - CONFIG ***************************/
|
||||
|
||||
/**
|
||||
* Custom hook to access the application's configuration context.
|
||||
*
|
||||
* This hook provides an easy way to consume the `ConfigContext`,
|
||||
* which typically contains application-wide settings like themes,
|
||||
* layouts, or other configurable options.
|
||||
*
|
||||
* @returns {any} The value provided by the `ConfigContext`.
|
||||
* Ensure that the component using this hook is wrapped with a `ConfigProvider`.
|
||||
*/
|
||||
|
||||
export default function useConfig() {
|
||||
const context = use(ConfigContext);
|
||||
|
||||
if (!context) throw new Error('useSConfig must be use inside ConfigProvider');
|
||||
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
/*************************** HOOKS - LOCAL STORAGE ***************************/
|
||||
|
||||
export default function useLocalStorage(key, defaultValue) {
|
||||
// Load initial state from localStorage or fallback to default
|
||||
const readValue = () => {
|
||||
if (typeof window === 'undefined') return defaultValue;
|
||||
|
||||
try {
|
||||
const item = localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : defaultValue;
|
||||
} catch (err) {
|
||||
console.warn(`Error reading localStorage key “${key}”:`, err);
|
||||
return defaultValue;
|
||||
}
|
||||
};
|
||||
|
||||
const [state, setState] = useState(readValue);
|
||||
|
||||
// Sync to localStorage whenever state changes
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(state));
|
||||
} catch (err) {
|
||||
console.warn(`Error setting localStorage key “${key}”:`, err);
|
||||
}
|
||||
}, [key, state]);
|
||||
|
||||
// Update single field
|
||||
const setField = useCallback((key, value) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
[key]: value
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Reset to defaults
|
||||
const resetState = useCallback(() => {
|
||||
setState(defaultValue);
|
||||
localStorage.setItem(key, JSON.stringify(defaultValue));
|
||||
}, [defaultValue, key]);
|
||||
|
||||
return { state, setState, setField, resetState };
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/*************************** MENU COLLAPSED - RECURSIVE FUNCTION ***************************/
|
||||
|
||||
/**
|
||||
* Recursively traverses menu items to find and open the correct parent menu.
|
||||
* If a menu item matches the current pathname, it marks the corresponding menu as selected and opens it.
|
||||
*
|
||||
* @param {NavItemType[]} items - List of menu items.
|
||||
* @param {string} pathname - Current route pathname.
|
||||
* @param {string | undefined} menuId - ID of the menu to be set as selected.
|
||||
* @param {SetState<string | null>} setSelected - Function to update the selected menu.
|
||||
* @param {Dispatch<SetStateAction<boolean>>} setOpen - Function to update the open state.
|
||||
*/
|
||||
|
||||
function setParentOpenedMenu(items, pathname, menuId, setSelected, setOpen) {
|
||||
for (const item of items) {
|
||||
// Recursively check child menus
|
||||
if (item.children?.length) {
|
||||
setParentOpenedMenu(item.children, pathname, menuId, setSelected, setOpen);
|
||||
}
|
||||
|
||||
if (item.url === pathname) {
|
||||
setSelected(menuId ?? null);
|
||||
setOpen(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*************************** MENU COLLAPSED - HOOK ***************************/
|
||||
|
||||
/**
|
||||
* Hook to handle menu collapse behavior based on the current route.
|
||||
* Automatically expands the parent menu of the active route item.
|
||||
*
|
||||
* @param {NavItemType} menu - The menu object containing items.
|
||||
* @param {string} pathname - Current route pathname.
|
||||
* @param {boolean} miniMenuOpened - Flag indicating if the mini menu is open.
|
||||
* @param {SetState<string | null>} setSelected - Function to update selected menu state.
|
||||
* @param {Dispatch<SetStateAction<boolean>>} setOpen - Function to update menu open state.
|
||||
* @param {SetState<HTMLElement>} setAnchorEl - Function to update the anchor element state.
|
||||
*/
|
||||
|
||||
export default function useMenuCollapse(menu, pathname, miniMenuOpened, setSelected, setOpen, setAnchorEl) {
|
||||
useEffect(() => {
|
||||
setOpen(false); // Close the menu initially
|
||||
|
||||
// Reset selection based on menu state
|
||||
if (!miniMenuOpened) {
|
||||
setSelected(null);
|
||||
} else {
|
||||
if (setAnchorEl) setAnchorEl(null);
|
||||
}
|
||||
|
||||
// If menu has children, determine which should be opened
|
||||
if (menu.children?.length) {
|
||||
setParentOpenedMenu(menu.children, pathname, menu.id, setSelected, setOpen);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname, menu.children]);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as DumpingDoodle } from './DumpingDoodle';
|
||||
export { default as ReadingSideDoodle } from './ReadingSideDoodle';
|
||||
@@ -0,0 +1,99 @@
|
||||
'use client';
|
||||
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
/*************************** IMAGE - ERROR 500 SERVER ***************************/
|
||||
|
||||
export default function Error5001() {
|
||||
const theme = useTheme();
|
||||
const primaryColor = theme.vars.palette.primary.dark;
|
||||
|
||||
return (
|
||||
<svg viewBox="0 0 1207 399" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ width: '100%', height: 'auto' }}>
|
||||
<path
|
||||
d="M845.5 315H390.498C368.407 315 350.498 297.091 350.498 275V275C350.498 252.909 368.407 235 390.498 235H459.112C481.418 235 499.5 216.918 499.5 194.612V194.612C499.5 172.75 481.75 155.041 459.888 155.092L256 155.563"
|
||||
stroke={primaryColor}
|
||||
strokeOpacity="0.4"
|
||||
strokeWidth="4.37971"
|
||||
/>
|
||||
<path
|
||||
opacity="0.5"
|
||||
d="M844.447 315.645C844.578 318.474 845.827 321.134 847.919 323.041C850.011 324.949 852.775 325.947 855.603 325.818L871.068 325.862L870.07 304.521L854.619 304.489C851.791 304.62 849.131 305.869 847.223 307.961C845.316 310.053 844.317 312.817 844.447 315.645Z"
|
||||
fill={primaryColor}
|
||||
/>
|
||||
<g opacity="0.6">
|
||||
<path
|
||||
d="M969.878 299.751L1002.05 299.179C1002.65 299.168 1003.25 299.023 1003.81 298.751C1004.37 298.48 1004.88 298.088 1005.31 297.598C1005.74 297.107 1006.08 296.528 1006.32 295.893C1006.56 295.258 1006.69 294.58 1006.7 293.898C1006.71 293.215 1006.6 292.541 1006.38 291.915C1006.16 291.289 1005.83 290.722 1005.41 290.247C1004.99 289.772 1004.49 289.399 1003.94 289.148C1003.39 288.897 1002.8 288.774 1002.2 288.785L970.035 289.369L969.878 299.751Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
<g opacity="0.6">
|
||||
<path
|
||||
d="M969.457 342.784L999.895 342.192C1000.58 342.183 1001.25 342.04 1001.89 341.77C1002.52 341.5 1003.1 341.11 1003.59 340.62C1004.08 340.131 1004.47 339.552 1004.74 338.918C1005.01 338.284 1005.16 337.606 1005.17 336.923C1005.19 336.24 1005.06 335.565 1004.81 334.937C1004.56 334.31 1004.19 333.742 1003.72 333.267C1003.24 332.791 1002.68 332.418 1002.05 332.168C1001.43 331.918 1000.75 331.796 1000.07 331.81L969.618 332.365L969.457 342.784Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M969.818 300.148C969.857 299.921 969.881 299.692 969.891 299.462L970.005 297.563L970.337 290.61L970.11 290.892L985.155 290.582L994.618 290.33L999.737 290.149L1002.34 290.095C1003.15 290.054 1003.94 290.269 1004.61 290.711C1005.18 291.076 1005.65 291.588 1005.97 292.201C1006.29 292.814 1006.45 293.509 1006.45 294.223C1006.44 294.918 1006.27 295.611 1005.95 296.243C1005.64 296.875 1005.19 297.43 1004.64 297.86C1004.12 298.261 1003.52 298.528 1002.89 298.639C1002.22 298.741 1001.55 298.79 1000.88 298.787L996.866 298.912L982.957 299.44L973.481 299.863L970.915 299.948C970.594 299.953 970.271 299.982 969.951 300.035C970.229 300.076 970.511 300.088 970.795 300.072L973.361 299.987C975.696 299.955 978.885 299.923 982.772 299.85L996.861 299.565L1000.93 299.469C1001.66 299.474 1002.39 299.434 1003.11 299.348C1004.13 299.177 1005.09 298.682 1005.86 297.933C1006.63 297.184 1007.16 296.218 1007.4 295.17C1007.63 294.121 1007.55 293.041 1007.16 292.081C1006.77 291.12 1006.1 290.327 1005.23 289.811C1004.43 289.298 1003.49 289.041 1002.52 289.069L999.93 289.133L994.823 289.325L985.353 289.638L970.324 290.213L970.067 290.221L970.071 290.474L969.994 297.577L970.017 299.41C969.921 299.648 969.854 299.896 969.818 300.148Z"
|
||||
fill="#263238"
|
||||
/>
|
||||
<path
|
||||
d="M969.916 342.454C969.953 342.205 969.966 341.951 969.957 341.698C969.979 341.141 970.004 340.466 970.016 339.634L970.15 332.074L969.937 332.374L984.552 332.508L993.726 332.555L998.693 332.523L1001.22 332.548C1002.01 332.549 1002.79 332.811 1003.45 333.305C1004.02 333.727 1004.48 334.303 1004.8 334.982C1005.13 335.662 1005.31 336.424 1005.32 337.202C1005.33 337.959 1005.19 338.708 1004.9 339.385C1004.61 340.062 1004.18 340.648 1003.66 341.092C1003.16 341.511 1002.59 341.786 1001.99 341.895C1001.34 341.975 1000.69 341.998 1000.04 341.965L996.143 341.997L982.635 342.111L973.444 342.264L970.954 342.274C970.645 342.259 970.336 342.285 970.032 342.349C970.032 342.349 970.295 342.386 970.84 342.405L973.292 342.479L982.429 342.635L996.093 342.768L1000.06 342.78C1000.76 342.824 1001.47 342.803 1002.17 342.72C1002.91 342.581 1003.61 342.232 1004.21 341.703C1004.84 341.176 1005.35 340.48 1005.69 339.675C1006.04 338.87 1006.21 337.979 1006.19 337.08C1006.17 336.158 1005.94 335.258 1005.55 334.458C1005.16 333.658 1004.6 332.981 1003.93 332.487C1003.14 331.904 1002.22 331.594 1001.28 331.592C1000.42 331.607 999.58 331.594 998.751 331.567L993.796 331.61L984.617 331.63L970.025 331.784L969.788 331.797L969.787 332.061C969.856 335.285 969.896 337.879 969.926 339.789L969.975 341.788C969.874 342.221 969.906 342.469 969.916 342.454Z"
|
||||
fill="#263238"
|
||||
/>
|
||||
<path
|
||||
opacity="0.5"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M896.775 279.407L896.779 280.568C889.549 280.953 882.733 284.063 877.703 289.27C872.674 294.478 869.803 301.398 869.67 308.637L869.502 319.443C869.382 326.889 872.17 334.089 877.273 339.514C882.375 344.938 889.392 348.16 896.832 348.495C896.89 351.085 897.959 353.551 899.811 355.363C901.664 357.175 904.152 358.19 906.743 358.191C909.166 358.188 911.504 357.299 913.318 355.692C915.132 354.085 916.296 351.871 916.591 349.466L970.354 350.565L970.178 314.688L970.177 317.835L932.412 317.823L932.415 310.45L970.158 310.462L969.994 277.153L916.43 277.328C915.92 274.924 914.538 272.795 912.55 271.35C910.562 269.906 908.109 269.25 905.665 269.509C903.222 269.767 900.96 270.922 899.318 272.75C897.676 274.578 896.77 276.95 896.775 279.407ZM970.158 310.462L970.178 314.688L970.18 310.462L970.158 310.462Z"
|
||||
fill={primaryColor}
|
||||
/>
|
||||
<path
|
||||
d="M896.78 281.41C896.639 282.297 896.574 283.194 896.585 284.093C896.498 285.829 896.431 288.328 896.379 291.42C896.251 297.605 896.24 306.154 896.313 315.591C896.386 325.029 896.465 333.411 896.536 339.762L896.619 347.062C896.595 347.965 896.654 348.869 896.794 349.762C896.935 348.875 897.001 347.978 896.99 347.08C897.076 345.343 897.144 342.844 897.195 339.752C897.323 333.567 897.334 325.019 897.202 315.652L897.038 291.41L896.941 284.099C896.973 283.199 896.919 282.299 896.78 281.41Z"
|
||||
fill="#263238"
|
||||
/>
|
||||
<path
|
||||
d="M916.473 278.111C916.326 279.05 916.266 280.001 916.292 280.951C916.241 282.789 916.203 285.457 916.169 288.732C916.13 295.304 916.195 304.373 916.348 314.384C916.501 324.394 916.684 333.32 916.805 340.049C916.892 343.207 916.884 345.78 916.984 347.815C916.969 348.771 917.033 349.727 917.174 350.673C917.3 349.727 917.355 348.774 917.341 347.821C917.391 345.983 917.444 343.326 917.464 340.04C917.503 333.467 917.438 324.398 917.285 314.388L916.816 288.737L916.649 280.957C916.663 280.005 916.604 279.054 916.473 278.111Z"
|
||||
fill="#263238"
|
||||
/>
|
||||
<g opacity="0.7">
|
||||
<path
|
||||
opacity="0.5"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M-60 3.33594C-60 1.67909 -58.6569 0.335938 -57 0.335938H246C251.523 0.335938 256 4.81309 256 10.3359V80.3359C256 85.8588 251.523 90.3359 246 90.3359H233V81.3359C233 79.6791 231.657 78.3359 230 78.3359H115C113.343 78.3359 112 79.6791 112 81.3359V90.3359H-60V3.33594ZM-15.442 25.8669C-15.442 32.9143 -21.155 38.6274 -28.2024 38.6274C-35.2498 38.6274 -40.9629 32.9143 -40.9629 25.8669C-40.9629 18.8195 -35.2498 13.1064 -28.2024 13.1064C-21.155 13.1064 -15.442 18.8195 -15.442 25.8669ZM193.51 22.6768C196.153 22.6768 198.295 20.5344 198.295 17.8916C198.295 15.2488 196.153 13.1064 193.51 13.1064C190.867 13.1064 188.725 15.2488 188.725 17.8916C188.725 20.5344 190.867 22.6768 193.51 22.6768ZM211.057 17.8916C211.057 20.5344 208.914 22.6768 206.272 22.6768C203.629 22.6768 201.486 20.5344 201.486 17.8916C201.486 15.2488 203.629 13.1064 206.272 13.1064C208.914 13.1064 211.057 15.2488 211.057 17.8916ZM219.031 22.6768C221.674 22.6768 223.816 20.5344 223.816 17.8916C223.816 15.2488 221.674 13.1064 219.031 13.1064C216.388 13.1064 214.246 15.2488 214.246 17.8916C214.246 20.5344 216.388 22.6768 219.031 22.6768ZM-9.0625 25.6768C-9.0625 24.0199 -7.71935 22.6768 -6.0625 22.6768H38.9793C40.6362 22.6768 41.9793 24.0199 41.9793 25.6768V29.057H-9.0625V25.6768Z"
|
||||
fill={primaryColor}
|
||||
/>
|
||||
<rect x="4" y="90.3359" width="189" height="13" fill={primaryColor} />
|
||||
<path
|
||||
opacity="0.5"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M-60 106.336C-60 104.679 -58.6569 103.336 -57 103.336H246C251.523 103.336 256 107.813 256 113.336V183.336C256 188.859 251.523 193.336 246 193.336H233V184.336C233 182.679 231.657 181.336 230 181.336H115C113.343 181.336 112 182.679 112 184.336V193.336H-60V106.336ZM-15.442 128.867C-15.442 135.914 -21.155 141.627 -28.2024 141.627C-35.2498 141.627 -40.9629 135.914 -40.9629 128.867C-40.9629 121.819 -35.2498 116.106 -28.2024 116.106C-21.155 116.106 -15.442 121.819 -15.442 128.867ZM193.51 125.677C196.153 125.677 198.295 123.534 198.295 120.892C198.295 118.249 196.153 116.106 193.51 116.106C190.867 116.106 188.725 118.249 188.725 120.892C188.725 123.534 190.867 125.677 193.51 125.677ZM211.057 120.892C211.057 123.534 208.914 125.677 206.272 125.677C203.629 125.677 201.486 123.534 201.486 120.892C201.486 118.249 203.629 116.106 206.272 116.106C208.914 116.106 211.057 118.249 211.057 120.892ZM219.031 125.677C221.674 125.677 223.816 123.534 223.816 120.892C223.816 118.249 221.674 116.106 219.031 116.106C216.388 116.106 214.246 118.249 214.246 120.892C214.246 123.534 216.388 125.677 219.031 125.677ZM-9.0625 128.677C-9.0625 127.02 -7.71935 125.677 -6.0625 125.677H38.9793C40.6362 125.677 41.9793 127.02 41.9793 128.677V132.057H-9.0625V128.677Z"
|
||||
fill={primaryColor}
|
||||
/>
|
||||
<rect x="4" y="193.336" width="189" height="13" fill={primaryColor} />
|
||||
<path
|
||||
opacity="0.5"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M-60 209.336C-60 207.679 -58.6569 206.336 -57 206.336H246C251.523 206.336 256 210.813 256 216.336V286.336C256 291.859 251.523 296.336 246 296.336H233V287.336C233 285.679 231.657 284.336 230 284.336H115C113.343 284.336 112 285.679 112 287.336V296.336H-60V209.336ZM-15.442 231.867C-15.442 238.914 -21.155 244.627 -28.2024 244.627C-35.2498 244.627 -40.9629 238.914 -40.9629 231.867C-40.9629 224.819 -35.2498 219.106 -28.2024 219.106C-21.155 219.106 -15.442 224.819 -15.442 231.867ZM193.51 228.677C196.153 228.677 198.295 226.534 198.295 223.892C198.295 221.249 196.153 219.106 193.51 219.106C190.867 219.106 188.725 221.249 188.725 223.892C188.725 226.534 190.867 228.677 193.51 228.677ZM211.057 223.892C211.057 226.534 208.914 228.677 206.272 228.677C203.629 228.677 201.486 226.534 201.486 223.892C201.486 221.249 203.629 219.106 206.272 219.106C208.914 219.106 211.057 221.249 211.057 223.892ZM219.031 228.677C221.674 228.677 223.816 226.534 223.816 223.892C223.816 221.249 221.674 219.106 219.031 219.106C216.388 219.106 214.246 221.249 214.246 223.892C214.246 226.534 216.388 228.677 219.031 228.677ZM-9.0625 231.677C-9.0625 230.02 -7.71935 228.677 -6.0625 228.677H38.9793C40.6362 228.677 41.9793 230.02 41.9793 231.677V235.057H-9.0625V231.677Z"
|
||||
fill={primaryColor}
|
||||
/>
|
||||
<rect x="4" y="296.336" width="189" height="13" fill={primaryColor} />
|
||||
<path
|
||||
opacity="0.5"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M-60 312C-60 310.343 -58.6569 309 -57 309H246C251.523 309 256 313.477 256 319V389C256 394.523 251.523 399 246 399H233V390C233 388.343 231.657 387 230 387H115C113.343 387 112 388.343 112 390V399H-60V312ZM-15.442 334.531C-15.442 341.578 -21.155 347.291 -28.2024 347.291C-35.2498 347.291 -40.9629 341.578 -40.9629 334.531C-40.9629 327.484 -35.2498 321.771 -28.2024 321.771C-21.155 321.771 -15.442 327.484 -15.442 334.531ZM193.51 331.341C196.153 331.341 198.295 329.198 198.295 326.556C198.295 323.913 196.153 321.771 193.51 321.771C190.867 321.771 188.725 323.913 188.725 326.556C188.725 329.198 190.867 331.341 193.51 331.341ZM211.057 326.556C211.057 329.198 208.914 331.341 206.272 331.341C203.629 331.341 201.486 329.198 201.486 326.556C201.486 323.913 203.629 321.771 206.272 321.771C208.914 321.771 211.057 323.913 211.057 326.556ZM219.031 331.341C221.674 331.341 223.816 329.198 223.816 326.556C223.816 323.913 221.674 321.771 219.031 321.771C216.388 321.771 214.246 323.913 214.246 326.556C214.246 329.198 216.388 331.341 219.031 331.341ZM-9.0625 334.341C-9.0625 332.684 -7.71935 331.341 -6.0625 331.341H38.9793C40.6362 331.341 41.9793 332.684 41.9793 334.341V337.721H-9.0625V334.341Z"
|
||||
fill={primaryColor}
|
||||
/>
|
||||
</g>
|
||||
<rect opacity="0.2" x="1110" y="155" width="97" height="97" rx="14" fill={primaryColor} />
|
||||
<circle opacity="0.4" cx="1158.5" cy="203.5" r="34.6429" fill={primaryColor} />
|
||||
<circle opacity="0.6" cx="1148.5" cy="203.5" r="7.5" fill={primaryColor} />
|
||||
<circle opacity="0.6" cx="1168.5" cy="203.5" r="7.5" fill={primaryColor} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @next
|
||||
import Link from 'next/link';
|
||||
|
||||
// @mui
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Button from '@mui/material/Button';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// @project
|
||||
import LogoSection from '@/components/logo';
|
||||
import MainCard from '@/components/MainCard';
|
||||
import { AvatarSize } from '@/enum';
|
||||
|
||||
// @assets
|
||||
import { IconBolt } from '@tabler/icons-react';
|
||||
|
||||
/*************************** NAVIGATION CARD - DATA ***************************/
|
||||
|
||||
const data = {
|
||||
title: 'Upgrade Your Experience',
|
||||
description: 'Take your experience to the next level with our premium offering. Buy now and enjoy more!',
|
||||
icon: <IconBolt size={16} />
|
||||
};
|
||||
|
||||
/*************************** NAVIGATION CARD - CONTENT ***************************/
|
||||
|
||||
function CardContent({ title, description, icon }) {
|
||||
return (
|
||||
<Stack sx={{ gap: 3 }}>
|
||||
<Stack direction="row" sx={{ gap: 0.25, alignItems: 'center' }}>
|
||||
<Avatar variant="rounded" size={AvatarSize.XS} sx={{ bgcolor: 'transparent' }}>
|
||||
<LogoSection isIcon sx={{ '& .MuiBox-root': { width: 'auto', height: 'auto' } }} />
|
||||
</Avatar>
|
||||
<Typography variant="body2">{process.env.NEXT_PUBLIC_VERSION}</Typography>
|
||||
</Stack>
|
||||
<Stack sx={{ gap: 1, alignItems: 'flex-start', textWrap: 'wrap' }}>
|
||||
<Typography variant="subtitle1">{title}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{description}
|
||||
</Typography>
|
||||
<Button
|
||||
startIcon={icon}
|
||||
variant="contained"
|
||||
component={Link}
|
||||
href={process.env.NEXT_PUBLIC_BUY_URL}
|
||||
target="_blank"
|
||||
sx={{ mt: 0.5 }}
|
||||
>
|
||||
Buy Now
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
/*************************** DRAWER CONTENT - NAVIGATION CARD ***************************/
|
||||
|
||||
export default function NavCard() {
|
||||
return (
|
||||
<MainCard sx={{ p: 1.5, bgcolor: 'grey.50', boxShadow: 'none', mb: 3 }}>
|
||||
<CardContent title={data.title} description={data.description} icon={data.icon} />
|
||||
</MainCard>
|
||||
);
|
||||
}
|
||||
|
||||
CardContent.propTypes = { title: PropTypes.string, description: PropTypes.string, icon: PropTypes.any };
|
||||
@@ -0,0 +1,110 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { Activity, useState } from 'react';
|
||||
|
||||
// @next
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import List from '@mui/material/List';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// @project
|
||||
import NavItem from './NavItem';
|
||||
import DynamicIcon from '@/components/DynamicIcon';
|
||||
import useMenuCollapse from '@/hooks/useMenuCollapse';
|
||||
|
||||
// @assets
|
||||
import { IconChevronDown, IconChevronUp } from '@tabler/icons-react';
|
||||
|
||||
// @style
|
||||
const verticalDivider = {
|
||||
'&:after': {
|
||||
content: "''",
|
||||
position: 'absolute',
|
||||
left: 16,
|
||||
top: -2,
|
||||
height: `calc(100% + 2px)`,
|
||||
width: '1px',
|
||||
opacity: 1,
|
||||
bgcolor: 'divider'
|
||||
}
|
||||
};
|
||||
|
||||
/*************************** COLLAPSE - LOOP ***************************/
|
||||
|
||||
function NavCollapseLoop({ item }) {
|
||||
return item.children?.map((item) => {
|
||||
switch (item.type) {
|
||||
case 'collapse':
|
||||
return <NavCollapse key={item.id} item={item} level={1} />;
|
||||
case 'item':
|
||||
return <NavItem key={item.id} item={item} level={1} />;
|
||||
default:
|
||||
return (
|
||||
<Typography key={item.id} variant="h6" color="error" align="center">
|
||||
Fix - Collapse or Item
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*************************** RESPONSIVE DRAWER - COLLAPSE ***************************/
|
||||
|
||||
export default function NavCollapse({ item, level = 0 }) {
|
||||
const theme = useTheme();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selected, setSelected] = useState(null);
|
||||
|
||||
// Active item collapse on page load with sub-levels
|
||||
const pathname = usePathname();
|
||||
|
||||
useMenuCollapse(item, pathname, false, setSelected, setOpen);
|
||||
|
||||
const handleClick = () => {
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
const iconcolor = theme.vars.palette.text.primary;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItemButton
|
||||
id={`${item.id}-btn`}
|
||||
selected={open || selected === item.id}
|
||||
sx={{
|
||||
my: 0.25,
|
||||
color: 'text.primary',
|
||||
'&.Mui-selected': {
|
||||
color: 'text.primary',
|
||||
'&.Mui-focusVisible': { bgcolor: 'primary.light' }
|
||||
}
|
||||
}}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Activity mode={level === 0 ? 'visible' : 'hidden'}>
|
||||
<ListItemIcon>
|
||||
<DynamicIcon name={item.icon} color={iconcolor} size={18} stroke={1.5} />
|
||||
</ListItemIcon>
|
||||
</Activity>
|
||||
<ListItemText primary={item.title} sx={{ mb: '-1px' }} />
|
||||
{open ? <IconChevronUp size={18} stroke={1.5} /> : <IconChevronDown size={18} stroke={1.5} />}
|
||||
</ListItemButton>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<List component="div" sx={{ p: 0, pl: 3, position: 'relative', ...verticalDivider }}>
|
||||
<NavCollapseLoop item={item} />
|
||||
</List>
|
||||
</Collapse>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
NavCollapseLoop.propTypes = { item: PropTypes.any };
|
||||
|
||||
NavCollapse.propTypes = { item: PropTypes.any, level: PropTypes.number };
|
||||
@@ -0,0 +1,44 @@
|
||||
import PropTypes from 'prop-types';
|
||||
// @mui
|
||||
import List from '@mui/material/List';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// @project
|
||||
import NavCollapse from './NavCollapse';
|
||||
import NavItem from './NavItem';
|
||||
|
||||
/*************************** RESPONSIVE DRAWER - GROUP ***************************/
|
||||
|
||||
export default function NavGroup({ item }) {
|
||||
const renderNavItem = (menuItem) => {
|
||||
// Render items based on the type
|
||||
switch (menuItem.type) {
|
||||
case 'collapse':
|
||||
return <NavCollapse key={menuItem.id} item={menuItem} />;
|
||||
case 'item':
|
||||
return <NavItem key={menuItem.id} item={menuItem} />;
|
||||
default:
|
||||
return (
|
||||
<Typography key={menuItem.id} variant="h6" color="error" align="center">
|
||||
Fix - Group Collapse or Items
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<List
|
||||
component="div"
|
||||
subheader={
|
||||
<Typography component="div" variant="caption" sx={{ mb: 0.75, color: 'grey.700' }}>
|
||||
{item.title}
|
||||
</Typography>
|
||||
}
|
||||
sx={{ '&:not(:first-of-type)': { pt: 1, borderTop: '1px solid', borderColor: 'divider' } }}
|
||||
>
|
||||
{item.children?.map((menuItem) => renderNavItem(menuItem))}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
NavGroup.propTypes = { item: PropTypes.any };
|
||||
@@ -0,0 +1,75 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { Activity, useEffect } from 'react';
|
||||
|
||||
// @next
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
|
||||
// @project
|
||||
import { handlerActiveItem, handlerDrawerOpen, useGetMenuMaster } from '@/states/menu';
|
||||
import DynamicIcon from '@/components/DynamicIcon';
|
||||
|
||||
/*************************** RESPONSIVE DRAWER - ITEM ***************************/
|
||||
|
||||
export default function NavItem({ item, level = 0 }) {
|
||||
const theme = useTheme();
|
||||
const { menuMaster } = useGetMenuMaster();
|
||||
const openItem = menuMaster.openedItem;
|
||||
|
||||
const downMD = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
// Active menu item on page load
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
if (pathname === item.url) handlerActiveItem(item.id);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname]);
|
||||
|
||||
const iconcolor = theme.vars.palette.text.primary;
|
||||
|
||||
const itemHandler = () => {
|
||||
if (downMD) handlerDrawerOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
id={`${item.id}-btn`}
|
||||
component={Link}
|
||||
href={item.url}
|
||||
{...(item?.target && { target: '_blank' })}
|
||||
selected={openItem === item.id}
|
||||
disabled={item.disabled}
|
||||
onClick={itemHandler}
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
...(level === 0 && { my: 0.25, '&.Mui-selected.Mui-focusVisible': { bgcolor: 'primary.light' } }),
|
||||
...(level > 0 && {
|
||||
'&.Mui-selected': {
|
||||
color: 'primary.main',
|
||||
bgcolor: 'transparent',
|
||||
'&:hover': { bgcolor: 'action.hover' },
|
||||
'&.Mui-focusVisible': { bgcolor: 'action.focus' },
|
||||
'& .MuiTypography-root': { fontWeight: 600 }
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Activity mode={level === 0 ? 'visible' : 'hidden'}>
|
||||
<ListItemIcon>
|
||||
<DynamicIcon name={item.icon} color={iconcolor} size={18} stroke={1.5} />
|
||||
</ListItemIcon>
|
||||
</Activity>
|
||||
<ListItemText primary={item.title} sx={{ mb: '-1px' }} />
|
||||
</ListItemButton>
|
||||
);
|
||||
}
|
||||
|
||||
NavItem.propTypes = { item: PropTypes.any, level: PropTypes.number };
|
||||
@@ -0,0 +1,26 @@
|
||||
// @mui
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// @project
|
||||
import menuItems from '@/menu';
|
||||
import NavGroup from './NavGroup';
|
||||
|
||||
/*************************** DRAWER CONTENT - RESPONSIVE DRAWER ***************************/
|
||||
|
||||
export default function ResponsiveDrawer() {
|
||||
const navGroups = menuItems.items.map((item, index) => {
|
||||
switch (item.type) {
|
||||
case 'group':
|
||||
return <NavGroup key={index} item={item} />;
|
||||
default:
|
||||
return (
|
||||
<Typography key={index} variant="h6" color="error" align="center">
|
||||
Fix - Navigation Group
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return <Box sx={{ py: 1, transition: 'all 0.3s ease-in-out' }}>{navGroups}</Box>;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import Stack from '@mui/material/Stack';
|
||||
|
||||
// @project
|
||||
import NavCard from './NavCard';
|
||||
import ResponsiveDrawer from './ResponsiveDrawer';
|
||||
|
||||
import { useGetMenuMaster } from '@/states/menu';
|
||||
import { MINI_DRAWER_WIDTH } from '@/config';
|
||||
import SimpleBar from '@/components/third-party/SimpleBar';
|
||||
|
||||
/*************************** DRAWER - CONTENT ***************************/
|
||||
|
||||
export default function DrawerContent() {
|
||||
const upMD = useMediaQuery((theme) => theme.breakpoints.up('lg'));
|
||||
|
||||
const { menuMaster } = useGetMenuMaster();
|
||||
const drawerOpen = menuMaster.isDashboardDrawerOpened;
|
||||
|
||||
const contentHeight = `calc(100vh - ${MINI_DRAWER_WIDTH}px)`;
|
||||
|
||||
return (
|
||||
<SimpleBar sx={{ height: contentHeight }}>
|
||||
<Stack sx={{ minHeight: contentHeight, px: !drawerOpen && upMD ? 0 : 2, justifyContent: 'space-between' }}>
|
||||
<ResponsiveDrawer />
|
||||
<NavCard />
|
||||
</Stack>
|
||||
</SimpleBar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { Activity } from 'react';
|
||||
|
||||
// @mui
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// @project
|
||||
import { handlerDrawerOpen, useGetMenuMaster } from '@/states/menu';
|
||||
import Logo from '@/components/logo';
|
||||
|
||||
// @assets
|
||||
import { IconLayoutSidebarLeftCollapse, IconLayoutSidebarRightCollapse } from '@tabler/icons-react';
|
||||
|
||||
/*************************** DRAWER HEADER ***************************/
|
||||
|
||||
export default function DrawerHeader({ open }) {
|
||||
const { menuMaster } = useGetMenuMaster();
|
||||
const drawerOpen = menuMaster.isDashboardDrawerOpened;
|
||||
|
||||
return (
|
||||
<Box sx={{ width: 1, px: 2, py: { xs: 2, md: 2.5 } }}>
|
||||
<Stack direction="row" sx={{ alignItems: 'center', justifyContent: open ? 'space-between' : 'center', height: 36 }}>
|
||||
<Activity mode={open ? 'visible' : 'hidden'}>
|
||||
<Logo />
|
||||
</Activity>
|
||||
<IconButton
|
||||
aria-label="open drawer"
|
||||
onClick={() => handlerDrawerOpen(!drawerOpen)}
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
>
|
||||
{!drawerOpen ? <IconLayoutSidebarRightCollapse size={20} /> : <IconLayoutSidebarLeftCollapse size={20} />}
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
DrawerHeader.propTypes = { open: PropTypes.bool };
|
||||
@@ -0,0 +1,53 @@
|
||||
// @mui
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
|
||||
// @project
|
||||
import { DRAWER_WIDTH } from '@/config';
|
||||
|
||||
// Mixin for common ) (open/closed) drawer state0....
|
||||
const commonDrawerStyles = (theme) => ({
|
||||
borderRight: `1px solid ${theme.vars.palette.grey[300]}`,
|
||||
overflowX: 'hidden'
|
||||
});
|
||||
|
||||
// Mixin for opened drawer state
|
||||
const openedMixin = (theme) => ({
|
||||
...commonDrawerStyles(theme),
|
||||
width: DRAWER_WIDTH,
|
||||
|
||||
transition: theme.transitions.create('width', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen
|
||||
})
|
||||
});
|
||||
|
||||
// Mixin for closed drawer state
|
||||
const closedMixin = (theme) => ({
|
||||
...commonDrawerStyles(theme),
|
||||
width: 0,
|
||||
|
||||
transition: theme.transitions.create('width', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen
|
||||
})
|
||||
});
|
||||
|
||||
/*************************** DRAWER - MINI STYLED ***************************/
|
||||
|
||||
const MiniDrawerStyled = styled(Drawer, { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({
|
||||
width: DRAWER_WIDTH,
|
||||
flexShrink: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
boxSizing: 'border-box',
|
||||
...(open && {
|
||||
...openedMixin(theme),
|
||||
'& .MuiDrawer-paper': openedMixin(theme)
|
||||
}),
|
||||
...(!open && {
|
||||
...closedMixin(theme),
|
||||
'& .MuiDrawer-paper': closedMixin(theme)
|
||||
})
|
||||
}));
|
||||
|
||||
export default MiniDrawerStyled;
|
||||
@@ -0,0 +1,69 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { Activity, useMemo } from 'react';
|
||||
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// @project
|
||||
import DrawerHeader from './DrawerHeader';
|
||||
import DrawerContent from './DrawerContent';
|
||||
import MiniDrawerStyled from './MiniDrawerStyled';
|
||||
|
||||
import { handlerDrawerOpen, useGetMenuMaster } from '@/states/menu';
|
||||
import { DRAWER_WIDTH } from '@/config';
|
||||
|
||||
/*************************** ADMIN LAYOUT - DRAWER ***************************/
|
||||
|
||||
export default function MainDrawer({ window }) {
|
||||
const { menuMaster } = useGetMenuMaster();
|
||||
const drawerOpen = menuMaster.isDashboardDrawerOpened;
|
||||
const downLG = useMediaQuery((theme) => theme.breakpoints.down('lg'));
|
||||
|
||||
// Define container for drawer when window is specified
|
||||
const container = window !== undefined ? () => window().document.body : undefined;
|
||||
|
||||
// Memoize drawer content and header to prevent unnecessary re-renders
|
||||
const drawerContent = useMemo(() => <DrawerContent />, []);
|
||||
const drawerHeader = useMemo(() => <DrawerHeader open={drawerOpen} />, [drawerOpen]);
|
||||
|
||||
return (
|
||||
<Box component="nav" sx={{ flexShrink: { md: 0 }, zIndex: 1200 }} aria-label="mailbox folders">
|
||||
{/* Temporary drawer for small media */}
|
||||
<Drawer
|
||||
container={container}
|
||||
variant="temporary"
|
||||
open={drawerOpen && downLG}
|
||||
onClose={() => handlerDrawerOpen(!drawerOpen)}
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
boxSizing: 'border-box',
|
||||
width: DRAWER_WIDTH,
|
||||
borderRight: '1px solid',
|
||||
borderRightColor: 'divider',
|
||||
backgroundImage: 'none',
|
||||
boxShadow: 'inherit'
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{drawerHeader}
|
||||
<Divider sx={{ mx: 2 }} />
|
||||
{drawerContent}
|
||||
</Drawer>
|
||||
|
||||
{/* Permanent mini-drawer for large media */}
|
||||
<Activity mode={!downLG ? 'visible' : 'hidden'}>
|
||||
<MiniDrawerStyled variant="permanent" open={drawerOpen}>
|
||||
{drawerHeader}
|
||||
<Divider sx={{ mx: 2 }} />
|
||||
{drawerContent}
|
||||
</MiniDrawerStyled>
|
||||
</Activity>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
MainDrawer.propTypes = { window: PropTypes.func };
|
||||
@@ -0,0 +1,26 @@
|
||||
// @mui
|
||||
import { styled } from '@mui/material/styles';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
|
||||
// @project
|
||||
import { DRAWER_WIDTH } from '@/config';
|
||||
|
||||
/*************************** HEADER - APP BAR STYLED ***************************/
|
||||
|
||||
const AppBarStyled = styled(AppBar, { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({
|
||||
zIndex: theme.zIndex.drawer + 1, // Ensure AppBar appears above the Drawer
|
||||
transition: theme.transitions.create(['width', 'margin'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen
|
||||
}),
|
||||
...(open && {
|
||||
marginLeft: DRAWER_WIDTH, // Shift AppBar to the right by the drawer width
|
||||
width: `calc(100% - ${DRAWER_WIDTH}px)`,
|
||||
transition: theme.transitions.create(['width', 'margin'], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen
|
||||
})
|
||||
})
|
||||
}));
|
||||
|
||||
export default AppBarStyled;
|
||||
@@ -0,0 +1,311 @@
|
||||
'use client';
|
||||
|
||||
import { Fragment, useState } from 'react';
|
||||
|
||||
// @mui
|
||||
import { keyframes, useTheme } from '@mui/material/styles';
|
||||
import Badge from '@mui/material/Badge';
|
||||
import Button from '@mui/material/Button';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardActions from '@mui/material/CardActions';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import Fade from '@mui/material/Fade';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import List from '@mui/material/List';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// @project
|
||||
import EmptyNotification from '@/components/header/empty-state/EmptyNotification';
|
||||
import MainCard from '@/components/MainCard';
|
||||
import NotificationItem from '@/components/NotificationItem';
|
||||
import SimpleBar from '@/components/third-party/SimpleBar';
|
||||
|
||||
// @assets
|
||||
import { IconBell, IconCode, IconChevronDown, IconGitBranch, IconNote, IconGps } from '@tabler/icons-react';
|
||||
|
||||
const swing = keyframes`
|
||||
20% {
|
||||
transform: rotate(15deg) scale(1);
|
||||
}
|
||||
40% {
|
||||
transform: rotate(-10deg) scale(1.05);
|
||||
}
|
||||
60% {
|
||||
transform: rotate(5deg) scale(1.1);
|
||||
}
|
||||
80% {
|
||||
transform: rotate(-5deg) scale(1.05);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(0deg) scale(1);
|
||||
}
|
||||
`;
|
||||
|
||||
/*************************** HEADER - NOTIFICATION ***************************/
|
||||
|
||||
export default function Notification() {
|
||||
const theme = useTheme();
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [innerAnchorEl, setInnerAnchorEl] = useState(null);
|
||||
const [allRead, setAllRead] = useState(false);
|
||||
const [showEmpty, setShowEmpty] = useState(false);
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
const innerOpen = Boolean(innerAnchorEl);
|
||||
const id = open ? 'notification-action-popper' : undefined;
|
||||
const innerId = innerOpen ? 'notification-inner-popper' : undefined;
|
||||
const buttonStyle = { borderRadius: 2, p: 1 };
|
||||
|
||||
const listcontent = ['All notification', 'Users', 'Account', 'Language', 'Role & Permission', 'Setting'];
|
||||
|
||||
const [notifications, setNotifications] = useState([
|
||||
{
|
||||
avatar: { alt: 'Travis Howard', src: '/assets/images/users/avatar-1.png' },
|
||||
badge: <IconCode size={14} />,
|
||||
title: 'New Feature Deployed · Code Review Needed',
|
||||
subTitle: 'Brenda Skiles',
|
||||
dateTime: 'Jul 9'
|
||||
},
|
||||
{
|
||||
avatar: <IconGitBranch />,
|
||||
title: 'New Branch Created - "feature-user-auth"',
|
||||
subTitle: 'Michael Carter',
|
||||
dateTime: 'Jul 10',
|
||||
isSeen: true
|
||||
},
|
||||
{
|
||||
avatar: <IconGitBranch />,
|
||||
title: 'Pull Request Opened "fix-dashboard-bug"',
|
||||
subTitle: 'Sophia Green',
|
||||
dateTime: 'Jul 11'
|
||||
},
|
||||
{
|
||||
avatar: { alt: 'Travis Howard', src: '/assets/images/users/avatar-4.png' },
|
||||
badge: <IconNote size={14} />,
|
||||
title: 'Admin Approval · Document Submission Accepted',
|
||||
subTitle: 'Salvatore Bogan',
|
||||
dateTime: 'Jul 15',
|
||||
isSeen: true
|
||||
},
|
||||
{
|
||||
avatar: <IconGps />,
|
||||
title: 'Location Access Request, Pending Your Approval',
|
||||
subTitle: 'System Notification',
|
||||
dateTime: 'Jul 24',
|
||||
isSeen: true
|
||||
}
|
||||
]);
|
||||
|
||||
const [notifications2, setNotifications2] = useState([
|
||||
{
|
||||
avatar: { alt: 'Travis Howard', src: '/assets/images/users/avatar-1.png' },
|
||||
badge: <IconCode size={14} />,
|
||||
title: 'Code Review Requested · Feature Deployment',
|
||||
subTitle: 'Brenda Skiles',
|
||||
dateTime: 'Jul 9'
|
||||
},
|
||||
{
|
||||
avatar: <IconGps />,
|
||||
title: 'Location Access Granted [Security Update]',
|
||||
subTitle: 'System Notification',
|
||||
dateTime: 'Jul 24',
|
||||
isSeen: true
|
||||
},
|
||||
{
|
||||
avatar: { alt: 'Alice Smith', src: '/assets/images/users/avatar-5.png' },
|
||||
badge: <IconNote size={14} />,
|
||||
title: 'Document Submission Approval Received',
|
||||
subTitle: 'Salvatore Bogan',
|
||||
dateTime: 'Aug 12',
|
||||
isSeen: true
|
||||
},
|
||||
{
|
||||
avatar: { alt: 'Travis Howard', src: '/assets/images/users/avatar-1.png' },
|
||||
badge: <IconCode size={14} />,
|
||||
title: 'New Commit Pushed · Review Changes',
|
||||
subTitle: 'Brenda Skiles',
|
||||
dateTime: 'Jul 9'
|
||||
},
|
||||
{
|
||||
avatar: <IconGps />,
|
||||
title: 'Unusual Login Attempt [Verify Activity]',
|
||||
subTitle: 'Security Alert',
|
||||
dateTime: 'Jul 24'
|
||||
}
|
||||
]);
|
||||
|
||||
const handleActionClick = (event) => {
|
||||
setAnchorEl(anchorEl ? null : event.currentTarget);
|
||||
};
|
||||
|
||||
const handleInnerActionClick = (event) => {
|
||||
setInnerAnchorEl(innerAnchorEl ? null : event.currentTarget);
|
||||
};
|
||||
|
||||
// Function to mark all notifications as read
|
||||
const handleMarkAllAsRead = () => {
|
||||
setNotifications((prevNotifications) => prevNotifications.map((notification) => ({ ...notification, isSeen: true })));
|
||||
setNotifications2((prevNotifications2) => prevNotifications2.map((notification) => ({ ...notification, isSeen: true })));
|
||||
setAllRead(true);
|
||||
};
|
||||
|
||||
const handleClearAll = () => {
|
||||
setNotifications([]);
|
||||
setNotifications2([]);
|
||||
setShowEmpty(true); // Set empty state to true when cleared
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="small"
|
||||
onClick={handleActionClick}
|
||||
aria-label="show notifications"
|
||||
{...(notifications.length !== 0 && !allRead && { sx: { '& svg': { animation: `${swing} 1s ease infinite` } } })}
|
||||
>
|
||||
<Badge
|
||||
color="error"
|
||||
variant="dot"
|
||||
invisible={allRead || notifications.length === 0}
|
||||
slotProps={{
|
||||
badge: { sx: { height: 6, minWidth: 6, top: 4, right: 4, border: `1px solid ${theme.vars.palette.background.default}` } }
|
||||
}}
|
||||
>
|
||||
<IconBell size={16} />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
<Popper
|
||||
placement="bottom-end"
|
||||
id={id}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
popperOptions={{
|
||||
modifiers: [{ name: 'offset', options: { offset: [0, 8] } }]
|
||||
}}
|
||||
transition
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Fade in={open} {...TransitionProps}>
|
||||
<MainCard
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
boxShadow: theme.vars.customShadows.tooltip,
|
||||
width: 1,
|
||||
minWidth: { xs: 352, sm: 240 },
|
||||
maxWidth: { xs: 352, md: 420 },
|
||||
p: 0
|
||||
}}
|
||||
>
|
||||
<ClickAwayListener onClickAway={() => setAnchorEl(null)}>
|
||||
<Box>
|
||||
<CardHeader
|
||||
sx={{ p: 1 }}
|
||||
title={
|
||||
<Stack direction="row" sx={{ gap: 1, justifyContent: 'space-between' }}>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="small"
|
||||
sx={{ typography: 'h6' }}
|
||||
endIcon={<IconChevronDown size={16} />}
|
||||
onClick={handleInnerActionClick}
|
||||
>
|
||||
All Notification
|
||||
</Button>
|
||||
<Popper
|
||||
placement="bottom-start"
|
||||
id={innerId}
|
||||
open={innerOpen}
|
||||
anchorEl={innerAnchorEl}
|
||||
transition
|
||||
popperOptions={{ modifiers: [{ name: 'preventOverflow', options: { boundary: 'clippingParents' } }] }}
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Fade in={innerOpen} {...TransitionProps}>
|
||||
<MainCard sx={{ borderRadius: 2, boxShadow: theme.vars.customShadows.tooltip, minWidth: 156, p: 0.5 }}>
|
||||
<ClickAwayListener onClickAway={() => setInnerAnchorEl(null)}>
|
||||
<List disablePadding>
|
||||
{listcontent.map((item, index) => (
|
||||
<ListItemButton key={index} sx={buttonStyle} onClick={handleInnerActionClick}>
|
||||
<ListItemText>{item}</ListItemText>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</ClickAwayListener>
|
||||
</MainCard>
|
||||
</Fade>
|
||||
)}
|
||||
</Popper>
|
||||
<Button color="primary" size="small" onClick={handleMarkAllAsRead} disabled={allRead}>
|
||||
Mark All as Read
|
||||
</Button>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
{showEmpty ? (
|
||||
<EmptyNotification />
|
||||
) : (
|
||||
<Fragment>
|
||||
<CardContent sx={{ px: 0.5, py: 2, '&:last-child': { pb: 2 } }}>
|
||||
<SimpleBar sx={{ maxHeight: 405, height: 1 }}>
|
||||
<List disablePadding>
|
||||
<ListSubheader disableSticky sx={{ color: 'text.disabled', typography: 'caption', py: 0.5, px: 1, mb: 0.5 }}>
|
||||
Last 7 Days
|
||||
</ListSubheader>
|
||||
{notifications.map((notification, index) => (
|
||||
<ListItemButton key={index} sx={buttonStyle}>
|
||||
<NotificationItem
|
||||
avatar={notification.avatar}
|
||||
{...(notification.badge && { badgeAvatar: { children: notification.badge } })}
|
||||
title={notification.title}
|
||||
subTitle={notification.subTitle}
|
||||
dateTime={notification.dateTime}
|
||||
isSeen={notification.isSeen}
|
||||
/>
|
||||
</ListItemButton>
|
||||
))}
|
||||
<ListSubheader
|
||||
disableSticky
|
||||
sx={{ color: 'text.disabled', typography: 'caption', py: 0.5, px: 1, mb: 0.5, mt: 1.5 }}
|
||||
>
|
||||
Older
|
||||
</ListSubheader>
|
||||
{notifications2.map((notification, index) => (
|
||||
<ListItemButton key={index} sx={buttonStyle}>
|
||||
<NotificationItem
|
||||
avatar={notification.avatar}
|
||||
{...(notification.badge && { badgeAvatar: { children: notification.badge } })}
|
||||
title={notification.title}
|
||||
subTitle={notification.subTitle}
|
||||
dateTime={notification.dateTime}
|
||||
isSeen={notification.isSeen}
|
||||
/>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</SimpleBar>
|
||||
</CardContent>
|
||||
<CardActions sx={{ p: 1 }}>
|
||||
<Button fullWidth color="error" onClick={handleClearAll}>
|
||||
Clear all
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Fragment>
|
||||
)}
|
||||
</Box>
|
||||
</ClickAwayListener>
|
||||
</MainCard>
|
||||
</Fade>
|
||||
)}
|
||||
</Popper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Button from '@mui/material/Button';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Fade from '@mui/material/Fade';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// @third-party
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
|
||||
// @project
|
||||
import { ThemeI18n } from '@/config';
|
||||
import MainCard from '@/components/MainCard';
|
||||
import Profile from '@/components/Profile';
|
||||
import { AvatarSize, ChipIconPosition } from '@/enum';
|
||||
import useConfig from '@/hooks/useConfig';
|
||||
|
||||
// @assets
|
||||
import { IconChevronRight, IconLanguage, IconLogout, IconSettings, IconTextDirectionLtr } from '@tabler/icons-react';
|
||||
|
||||
/*************************** HEADER - PROFILE DATA ***************************/
|
||||
|
||||
const profileData = {
|
||||
avatar: { src: '/assets/images/users/avatar-1.png', size: AvatarSize.XS },
|
||||
title: 'Erika Collins',
|
||||
caption: 'Super Admin'
|
||||
};
|
||||
|
||||
const languageList = [
|
||||
{ key: ThemeI18n.EN, value: 'English' },
|
||||
{ key: ThemeI18n.FR, value: 'French' },
|
||||
{ key: ThemeI18n.RO, value: 'Romanian' },
|
||||
{ key: ThemeI18n.ZH, value: 'Chinese' }
|
||||
];
|
||||
|
||||
/*************************** HEADER - PROFILE ***************************/
|
||||
|
||||
export default function ProfileSection() {
|
||||
const theme = useTheme();
|
||||
const {
|
||||
state: { i18n }
|
||||
} = useConfig();
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [innerAnchorEl, setInnerAnchorEl] = useState(null);
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
const innerOpen = Boolean(innerAnchorEl);
|
||||
const id = open ? 'profile-action-popper' : undefined;
|
||||
const innerId = innerOpen ? 'profile-inner-popper' : undefined;
|
||||
const buttonStyle = { borderRadius: 2, p: 1 };
|
||||
|
||||
const handleActionClick = (event) => {
|
||||
setAnchorEl(anchorEl ? null : event.currentTarget);
|
||||
};
|
||||
|
||||
const handleInnerActionClick = (event) => {
|
||||
setInnerAnchorEl(innerAnchorEl ? null : event.currentTarget);
|
||||
};
|
||||
|
||||
const logoutAccount = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const i18nHandler = (event, key) => {
|
||||
handleInnerActionClick(event);
|
||||
if (key != i18n) enqueueSnackbar('Upgrade to pro for language change');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box onClick={handleActionClick} sx={{ cursor: 'pointer' }}>
|
||||
<Box sx={{ display: { xs: 'none', sm: 'flex' } }}>
|
||||
<Profile {...profileData} />
|
||||
</Box>
|
||||
<Box sx={{ display: { xs: 'block', sm: 'none' } }}>
|
||||
<Avatar {...profileData.avatar} alt={profileData.title} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Popper
|
||||
placement="bottom-end"
|
||||
id={id}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
transition
|
||||
popperOptions={{ modifiers: [{ name: 'offset', options: { offset: [8, 8] } }] }}
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Fade in={open} {...TransitionProps}>
|
||||
<MainCard sx={{ borderRadius: 2, boxShadow: theme.vars.customShadows.tooltip, minWidth: 220, p: 0.5 }}>
|
||||
<ClickAwayListener onClickAway={() => setAnchorEl(null)}>
|
||||
<Stack sx={{ px: 0.5, py: 0.75 }}>
|
||||
<Profile
|
||||
{...profileData}
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
width: 1,
|
||||
'& .MuiAvatar-root': { width: 48, height: 48 }
|
||||
}}
|
||||
/>
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<List disablePadding>
|
||||
<ListItem
|
||||
secondaryAction={<Switch size="small" onChange={() => enqueueSnackbar('Upgrade to pro for RTL')} />}
|
||||
sx={{ py: 1, pl: 1, '& .MuiListItemSecondaryAction-root': { right: 8 } }}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<IconTextDirectionLtr size={16} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="RTL" />
|
||||
</ListItem>
|
||||
<ListItemButton sx={buttonStyle} onClick={handleInnerActionClick}>
|
||||
<ListItemIcon>
|
||||
<IconLanguage size={16} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Language" />
|
||||
<Chip
|
||||
label={languageList.filter((item) => item.key === i18n)[0]?.value.slice(0, 3)}
|
||||
variant="text"
|
||||
size="small"
|
||||
color="secondary"
|
||||
icon={<IconChevronRight size={16} />}
|
||||
position={ChipIconPosition.RIGHT}
|
||||
sx={{ textTransform: 'capitalize' }}
|
||||
/>
|
||||
<Popper
|
||||
placement="left-start"
|
||||
id={innerId}
|
||||
open={innerOpen}
|
||||
anchorEl={innerAnchorEl}
|
||||
transition
|
||||
popperOptions={{
|
||||
modifiers: [
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
boundary: 'clippingParents'
|
||||
}
|
||||
},
|
||||
{ name: 'offset', options: { offset: [0, 8] } }
|
||||
]
|
||||
}}
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Fade in={innerOpen} {...TransitionProps}>
|
||||
<MainCard sx={{ borderRadius: 2, boxShadow: theme.vars.customShadows.tooltip, minWidth: 150, p: 0.5 }}>
|
||||
<ClickAwayListener onClickAway={() => setInnerAnchorEl(null)}>
|
||||
<List disablePadding>
|
||||
{languageList.map((item, index) => (
|
||||
<ListItemButton
|
||||
selected={item.key === i18n}
|
||||
key={index}
|
||||
sx={buttonStyle}
|
||||
onClick={(event) => i18nHandler(event, item.key)}
|
||||
>
|
||||
<ListItemText>{item.value}</ListItemText>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</ClickAwayListener>
|
||||
</MainCard>
|
||||
</Fade>
|
||||
)}
|
||||
</Popper>
|
||||
</ListItemButton>
|
||||
<ListItemButton href="#" sx={{ ...buttonStyle, my: 0.5 }}>
|
||||
<ListItemIcon>
|
||||
<IconSettings size={16} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Settings" />
|
||||
</ListItemButton>
|
||||
<ListItem disablePadding>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="small"
|
||||
endIcon={<IconLogout size={16} />}
|
||||
onClick={logoutAccount}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Stack>
|
||||
</ClickAwayListener>
|
||||
</MainCard>
|
||||
</Fade>
|
||||
)}
|
||||
</Popper>
|
||||
</>
|
||||
);
|
||||
}
|
||||