first commit
64
app/401.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
// ** Next Import
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
// ** MUI Components
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import { styled } from '@mui/material/styles'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import Box, { BoxProps } from '@mui/material/Box'
|
||||||
|
|
||||||
|
// ** Layout Import
|
||||||
|
import BlankLayout from 'src/@core/layouts/BlankLayout'
|
||||||
|
|
||||||
|
// ** Demo Imports
|
||||||
|
import FooterIllustrations from 'src/views/pages/misc/FooterIllustrations'
|
||||||
|
|
||||||
|
// ** Styled Components
|
||||||
|
const BoxWrapper = styled(Box)<BoxProps>(({ theme }) => ({
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
width: '90vw'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const Img = styled('img')(({ theme }) => ({
|
||||||
|
[theme.breakpoints.down('lg')]: {
|
||||||
|
height: 450,
|
||||||
|
marginTop: theme.spacing(10)
|
||||||
|
},
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
height: 400
|
||||||
|
},
|
||||||
|
[theme.breakpoints.up('lg')]: {
|
||||||
|
marginTop: theme.spacing(20)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const Error401 = () => {
|
||||||
|
return (
|
||||||
|
<Box className='content-center'>
|
||||||
|
<Box sx={{ p: 5, display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }}>
|
||||||
|
<BoxWrapper>
|
||||||
|
<Typography variant='h4' sx={{ mb: 1.5 }}>
|
||||||
|
You are not authorized!
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: 'text.secondary' }}>
|
||||||
|
You do not have permission to view this page using the credentials that you have provided while login.
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ mb: 6, color: 'text.secondary' }}>Please contact your site administrator.</Typography>
|
||||||
|
<Button href='/' component={Link} variant='contained'>
|
||||||
|
Back to Home
|
||||||
|
</Button>
|
||||||
|
</BoxWrapper>
|
||||||
|
<Img height='500' alt='error-illustration' src='/images/pages/401.png' />
|
||||||
|
</Box>
|
||||||
|
<FooterIllustrations />
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Error401.getLayout = (page: ReactNode) => <BlankLayout>{page}</BlankLayout>
|
||||||
|
|
||||||
|
export default Error401
|
||||||
63
app/404.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
// ** Next Import
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
// ** MUI Components
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import { styled } from '@mui/material/styles'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import Box, { BoxProps } from '@mui/material/Box'
|
||||||
|
|
||||||
|
// ** Layout Import
|
||||||
|
import BlankLayout from 'src/@core/layouts/BlankLayout'
|
||||||
|
|
||||||
|
// ** Demo Imports
|
||||||
|
import FooterIllustrations from 'src/views/pages/misc/FooterIllustrations'
|
||||||
|
|
||||||
|
// ** Styled Components
|
||||||
|
const BoxWrapper = styled(Box)<BoxProps>(({ theme }) => ({
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
width: '90vw'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const Img = styled('img')(({ theme }) => ({
|
||||||
|
[theme.breakpoints.down('lg')]: {
|
||||||
|
height: 450,
|
||||||
|
marginTop: theme.spacing(10)
|
||||||
|
},
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
height: 400
|
||||||
|
},
|
||||||
|
[theme.breakpoints.up('lg')]: {
|
||||||
|
marginTop: theme.spacing(20)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const Error404 = () => {
|
||||||
|
return (
|
||||||
|
<Box className='content-center'>
|
||||||
|
<Box sx={{ p: 5, display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }}>
|
||||||
|
<BoxWrapper>
|
||||||
|
<Typography variant='h4' sx={{ mb: 1.5 }}>
|
||||||
|
Page Not Found :(
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ mb: 6, color: 'text.secondary' }}>
|
||||||
|
Oops! 😖 The requested URL was not found on this server.
|
||||||
|
</Typography>
|
||||||
|
<Button href='/' component={Link} variant='contained'>
|
||||||
|
Back to Home
|
||||||
|
</Button>
|
||||||
|
</BoxWrapper>
|
||||||
|
<Img height='500' alt='error-illustration' src='/images/pages/404.png' />
|
||||||
|
</Box>
|
||||||
|
<FooterIllustrations />
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Error404.getLayout = (page: ReactNode) => <BlankLayout>{page}</BlankLayout>
|
||||||
|
|
||||||
|
export default Error404
|
||||||
63
app/500.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
// ** Next Import
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
// ** MUI Components
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import { styled } from '@mui/material/styles'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import Box, { BoxProps } from '@mui/material/Box'
|
||||||
|
|
||||||
|
// ** Layout Import
|
||||||
|
import BlankLayout from 'src/@core/layouts/BlankLayout'
|
||||||
|
|
||||||
|
// ** Demo Imports
|
||||||
|
import FooterIllustrations from 'src/views/pages/misc/FooterIllustrations'
|
||||||
|
|
||||||
|
// ** Styled Components
|
||||||
|
const BoxWrapper = styled(Box)<BoxProps>(({ theme }) => ({
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
width: '90vw'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const Img = styled('img')(({ theme }) => ({
|
||||||
|
[theme.breakpoints.down('lg')]: {
|
||||||
|
height: 450,
|
||||||
|
marginTop: theme.spacing(10)
|
||||||
|
},
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
height: 400
|
||||||
|
},
|
||||||
|
[theme.breakpoints.up('lg')]: {
|
||||||
|
marginTop: theme.spacing(20)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const Error500 = () => {
|
||||||
|
return (
|
||||||
|
<Box className='content-center'>
|
||||||
|
<Box sx={{ p: 5, display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }}>
|
||||||
|
<BoxWrapper>
|
||||||
|
<Typography variant='h4' sx={{ mb: 1.5 }}>
|
||||||
|
Oops, something went wrong!
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ mb: 6, color: 'text.secondary' }}>
|
||||||
|
There was an error with the internal server. Please contact your site administrator.
|
||||||
|
</Typography>
|
||||||
|
<Button href='/' component={Link} variant='contained'>
|
||||||
|
Back to Home
|
||||||
|
</Button>
|
||||||
|
</BoxWrapper>
|
||||||
|
<Img height='500' alt='error-illustration' src='/images/pages/404.png' />
|
||||||
|
</Box>
|
||||||
|
<FooterIllustrations />
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Error500.getLayout = (page: ReactNode) => <BlankLayout>{page}</BlankLayout>
|
||||||
|
|
||||||
|
export default Error500
|
||||||
161
app/_app.tsx
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
// ** Next Imports
|
||||||
|
import Head from 'next/head'
|
||||||
|
import { Router } from 'next/router'
|
||||||
|
import type { NextPage } from 'next'
|
||||||
|
import type { AppProps } from 'next/app'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ** Loader Import
|
||||||
|
import NProgress from 'nprogress'
|
||||||
|
|
||||||
|
// ** Emotion Imports
|
||||||
|
import { CacheProvider } from '@emotion/react'
|
||||||
|
import type { EmotionCache } from '@emotion/cache'
|
||||||
|
|
||||||
|
// ** Config Imports
|
||||||
|
|
||||||
|
import { defaultACLObj } from 'src/configs/acl'
|
||||||
|
import themeConfig from 'src/configs/themeConfig'
|
||||||
|
|
||||||
|
// ** Fake-DB Import
|
||||||
|
import 'src/@fake-db'
|
||||||
|
|
||||||
|
// ** Third Party Import
|
||||||
|
import { Toaster } from 'react-hot-toast'
|
||||||
|
|
||||||
|
// ** Component Imports
|
||||||
|
import UserLayout from 'src/layouts/UserLayout'
|
||||||
|
import AclGuard from 'src/@core/components/auth/AclGuard'
|
||||||
|
import ThemeComponent from 'src/@core/theme/ThemeComponent'
|
||||||
|
import AuthGuard from 'src/@core/components/auth/AuthGuard'
|
||||||
|
import GuestGuard from 'src/@core/components/auth/GuestGuard'
|
||||||
|
import WindowWrapper from 'src/@core/components/window-wrapper'
|
||||||
|
|
||||||
|
// ** Spinner Import
|
||||||
|
import Spinner from 'src/@core/components/spinner'
|
||||||
|
|
||||||
|
// ** Contexts
|
||||||
|
import { AuthProvider } from 'src/context/AuthContext'
|
||||||
|
import { SettingsConsumer, SettingsProvider } from 'src/@core/context/settingsContext'
|
||||||
|
|
||||||
|
// ** Styled Components
|
||||||
|
import ReactHotToast from 'src/@core/styles/libs/react-hot-toast'
|
||||||
|
|
||||||
|
// ** Utils Imports
|
||||||
|
import { createEmotionCache } from 'src/@core/utils/create-emotion-cache'
|
||||||
|
|
||||||
|
// ** Prismjs Styles
|
||||||
|
import 'prismjs'
|
||||||
|
import 'prismjs/themes/prism-tomorrow.css'
|
||||||
|
import 'prismjs/components/prism-jsx'
|
||||||
|
import 'prismjs/components/prism-tsx'
|
||||||
|
|
||||||
|
// ** React Perfect Scrollbar Style
|
||||||
|
import 'react-perfect-scrollbar/dist/css/styles.css'
|
||||||
|
|
||||||
|
import 'src/iconify-bundle/icons-bundle-react'
|
||||||
|
|
||||||
|
// ** Global css styles
|
||||||
|
import '../../styles/globals.css'
|
||||||
|
|
||||||
|
// ** Extend App Props with Emotion
|
||||||
|
type ExtendedAppProps = AppProps & {
|
||||||
|
Component: NextPage
|
||||||
|
emotionCache: EmotionCache
|
||||||
|
}
|
||||||
|
|
||||||
|
type GuardProps = {
|
||||||
|
authGuard: boolean
|
||||||
|
guestGuard: boolean
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientSideEmotionCache = createEmotionCache()
|
||||||
|
|
||||||
|
// ** Pace Loader
|
||||||
|
if (themeConfig.routingLoader) {
|
||||||
|
Router.events.on('routeChangeStart', () => {
|
||||||
|
NProgress.start()
|
||||||
|
})
|
||||||
|
Router.events.on('routeChangeError', () => {
|
||||||
|
NProgress.done()
|
||||||
|
})
|
||||||
|
Router.events.on('routeChangeComplete', () => {
|
||||||
|
NProgress.done()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const Guard = ({ children, authGuard, guestGuard }: GuardProps) => {
|
||||||
|
if (guestGuard) {
|
||||||
|
return <GuestGuard fallback={<Spinner />}>{children}</GuestGuard>
|
||||||
|
} else if (!guestGuard && !authGuard) {
|
||||||
|
return <>{children}</>
|
||||||
|
} else {
|
||||||
|
return <AuthGuard fallback={<Spinner />}>{children}</AuthGuard>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ** Configure JSS & ClassName
|
||||||
|
const App = (props: ExtendedAppProps) => {
|
||||||
|
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
const contentHeightFixed = Component.contentHeightFixed ?? false
|
||||||
|
const getLayout =
|
||||||
|
Component.getLayout ?? (page => <UserLayout contentHeightFixed={contentHeightFixed}>{page}</UserLayout>)
|
||||||
|
|
||||||
|
const setConfig = Component.setConfig ?? undefined
|
||||||
|
|
||||||
|
const authGuard = Component.authGuard ?? true
|
||||||
|
|
||||||
|
const guestGuard = Component.guestGuard ?? false
|
||||||
|
|
||||||
|
const aclAbilities = Component.acl ?? defaultACLObj
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<CacheProvider value={emotionCache}>
|
||||||
|
<Head>
|
||||||
|
<title>{`${themeConfig.templateName} - Material Design React Admin Template`}</title>
|
||||||
|
<meta
|
||||||
|
name='description'
|
||||||
|
content={`${themeConfig.templateName} – Material Design React Admin Dashboard Template – is the most developer friendly & highly customizable Admin Dashboard Template based on MUI v5.`}
|
||||||
|
/>
|
||||||
|
<meta name='keywords' content='Material Design, MUI, Admin Template, React Admin Template' />
|
||||||
|
<meta name='viewport' content='initial-scale=1, width=device-width' />
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<AuthProvider>
|
||||||
|
<SettingsProvider {...(setConfig ? { pageSettings: setConfig() } : {})}>
|
||||||
|
<SettingsConsumer>
|
||||||
|
{({ settings }) => {
|
||||||
|
return (
|
||||||
|
<ThemeComponent settings={settings}>
|
||||||
|
<WindowWrapper>
|
||||||
|
<Guard authGuard={authGuard} guestGuard={guestGuard}>
|
||||||
|
<AclGuard aclAbilities={aclAbilities} guestGuard={guestGuard}>
|
||||||
|
{getLayout(<Component {...pageProps} />)}
|
||||||
|
</AclGuard>
|
||||||
|
</Guard>
|
||||||
|
</WindowWrapper>
|
||||||
|
<ReactHotToast>
|
||||||
|
<Toaster position={settings.toastPosition} toastOptions={{ className: 'react-hot-toast' }} />
|
||||||
|
</ReactHotToast>
|
||||||
|
</ThemeComponent>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</SettingsConsumer>
|
||||||
|
</SettingsProvider>
|
||||||
|
</AuthProvider>
|
||||||
|
</CacheProvider>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
70
app/_document.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// ** React Import
|
||||||
|
import { Children } from 'react'
|
||||||
|
|
||||||
|
// ** Next Import
|
||||||
|
import Document, { Html, Head, Main, NextScript } from 'next/document'
|
||||||
|
|
||||||
|
// ** Emotion Imports
|
||||||
|
import createEmotionServer from '@emotion/server/create-instance'
|
||||||
|
|
||||||
|
// ** Utils Imports
|
||||||
|
import { createEmotionCache } from 'src/@core/utils/create-emotion-cache'
|
||||||
|
|
||||||
|
class CustomDocument extends Document {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Html lang='en'>
|
||||||
|
<Head>
|
||||||
|
<link rel='preconnect' href='https://fonts.googleapis.com' />
|
||||||
|
<link rel='preconnect' href='https://fonts.gstatic.com' />
|
||||||
|
<link
|
||||||
|
rel='stylesheet'
|
||||||
|
href='https://fonts.googleapis.com/css2?family=Public+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap'
|
||||||
|
/>
|
||||||
|
<link rel='apple-touch-icon' sizes='180x180' href='/images/apple-touch-icon.png' />
|
||||||
|
<link rel='shortcut icon' href='/images/favicon.png' />
|
||||||
|
</Head>
|
||||||
|
<body>
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomDocument.getInitialProps = async ctx => {
|
||||||
|
const originalRenderPage = ctx.renderPage
|
||||||
|
const cache = createEmotionCache()
|
||||||
|
const { extractCriticalToChunks } = createEmotionServer(cache)
|
||||||
|
|
||||||
|
ctx.renderPage = () =>
|
||||||
|
originalRenderPage({
|
||||||
|
enhanceApp: App => props =>
|
||||||
|
(
|
||||||
|
<App
|
||||||
|
{...props} // @ts-ignore
|
||||||
|
emotionCache={cache}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const initialProps = await Document.getInitialProps(ctx)
|
||||||
|
const emotionStyles = extractCriticalToChunks(initialProps.html)
|
||||||
|
const emotionStyleTags = emotionStyles.styles.map(style => {
|
||||||
|
return (
|
||||||
|
<style
|
||||||
|
key={style.key}
|
||||||
|
dangerouslySetInnerHTML={{ __html: style.css }}
|
||||||
|
data-emotion={`${style.key} ${style.ids.join(' ')}`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
...initialProps,
|
||||||
|
styles: [...Children.toArray(initialProps.styles), ...emotionStyleTags]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomDocument
|
||||||
49
app/acl/index.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { useContext } from 'react'
|
||||||
|
|
||||||
|
// ** Context Imports
|
||||||
|
import { AbilityContext } from '../layouts/components/acl/Can'
|
||||||
|
|
||||||
|
// ** MUI Imports
|
||||||
|
import Grid from '@mui/material/Grid'
|
||||||
|
import Card from '@mui/material/Card'
|
||||||
|
import CardHeader from '@mui/material/CardHeader'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import CardContent from '@mui/material/CardContent'
|
||||||
|
|
||||||
|
const ACLPage = () => {
|
||||||
|
// ** Hooks
|
||||||
|
const ability = useContext(AbilityContext)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container spacing={6}>
|
||||||
|
<Grid item md={6} xs={12}>
|
||||||
|
<Card>
|
||||||
|
<CardHeader title='Common' />
|
||||||
|
<CardContent>
|
||||||
|
<Typography sx={{ mb: 4 }}>No ability is required to view this card</Typography>
|
||||||
|
<Typography sx={{ color: 'primary.main' }}>This card is visible to 'user' and 'admin' both</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
{ability?.can('read', 'analytics') ? (
|
||||||
|
<Grid item md={6} xs={12}>
|
||||||
|
<Card>
|
||||||
|
<CardHeader title='Analytics' />
|
||||||
|
<CardContent>
|
||||||
|
<Typography sx={{ mb: 4 }}>User with 'Analytics' subject's 'Read' ability can view this card</Typography>
|
||||||
|
<Typography sx={{ color: 'error.main' }}>This card is visible to 'admin' only</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
) : null}
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ACLPage.acl = {
|
||||||
|
action: 'read',
|
||||||
|
subject: 'acl-page'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ACLPage
|
||||||
159
app/forgot-password/page.tsx
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
// ** Next Import
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
// ** MUI Components
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import TextField from '@mui/material/TextField'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import Box, { BoxProps } from '@mui/material/Box'
|
||||||
|
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||||
|
import { styled, useTheme } from '@mui/material/styles'
|
||||||
|
|
||||||
|
// ** Icon Imports
|
||||||
|
import Icon from '../../src/@core/components/icon'
|
||||||
|
|
||||||
|
// ** Layout Import
|
||||||
|
import BlankLayout from '../../src/@core/layouts/BlankLayout'
|
||||||
|
|
||||||
|
// ** Demo Imports
|
||||||
|
import FooterIllustrationsV2 from '../../src/views/pages/auth/FooterIllustrationsV2'
|
||||||
|
|
||||||
|
// Styled Components
|
||||||
|
const ForgotPasswordIllustration = styled('img')(({ theme }) => ({
|
||||||
|
zIndex: 2,
|
||||||
|
maxHeight: 650,
|
||||||
|
marginTop: theme.spacing(12),
|
||||||
|
marginBottom: theme.spacing(12),
|
||||||
|
[theme.breakpoints.down(1540)]: {
|
||||||
|
maxHeight: 550
|
||||||
|
},
|
||||||
|
[theme.breakpoints.down('lg')]: {
|
||||||
|
maxHeight: 500
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const RightWrapper = styled(Box)<BoxProps>(({ theme }) => ({
|
||||||
|
width: '100%',
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
maxWidth: 450
|
||||||
|
},
|
||||||
|
[theme.breakpoints.up('lg')]: {
|
||||||
|
maxWidth: 600
|
||||||
|
},
|
||||||
|
[theme.breakpoints.up('xl')]: {
|
||||||
|
maxWidth: 750
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const LinkStyled = styled(Link)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
fontSize: '1rem',
|
||||||
|
alignItems: 'center',
|
||||||
|
textDecoration: 'none',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: theme.palette.primary.main
|
||||||
|
}))
|
||||||
|
|
||||||
|
const ForgotPassword = () => {
|
||||||
|
// ** Hooks
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
// ** Vars
|
||||||
|
const hidden = useMediaQuery(theme.breakpoints.down('md'))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className='content-right' sx={{ backgroundColor: 'background.paper' }}>
|
||||||
|
{!hidden ? (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
position: 'relative',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: '20px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: 'customColors.bodyBg',
|
||||||
|
margin: theme => theme.spacing(8, 0, 8, 8)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ForgotPasswordIllustration
|
||||||
|
alt='forgot-password-illustration'
|
||||||
|
src={`/images/pages/auth-v2-forgot-password-illustration-${theme.palette.mode}.png`}
|
||||||
|
/>
|
||||||
|
<FooterIllustrationsV2 />
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
<RightWrapper>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: [6, 12],
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ width: '100%', maxWidth: 400 }}>
|
||||||
|
<svg width={34} height={23.375} viewBox='0 0 32 22' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||||
|
<path
|
||||||
|
fillRule='evenodd'
|
||||||
|
clipRule='evenodd'
|
||||||
|
fill={theme.palette.primary.main}
|
||||||
|
d='M0.00172773 0V6.85398C0.00172773 6.85398 -0.133178 9.01207 1.98092 10.8388L13.6912 21.9964L19.7809 21.9181L18.8042 9.88248L16.4951 7.17289L9.23799 0H0.00172773Z'
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill='#161616'
|
||||||
|
opacity={0.06}
|
||||||
|
fillRule='evenodd'
|
||||||
|
clipRule='evenodd'
|
||||||
|
d='M7.69824 16.4364L12.5199 3.23696L16.5541 7.25596L7.69824 16.4364Z'
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill='#161616'
|
||||||
|
opacity={0.06}
|
||||||
|
fillRule='evenodd'
|
||||||
|
clipRule='evenodd'
|
||||||
|
d='M8.07751 15.9175L13.9419 4.63989L16.5849 7.28475L8.07751 15.9175Z'
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule='evenodd'
|
||||||
|
clipRule='evenodd'
|
||||||
|
fill={theme.palette.primary.main}
|
||||||
|
d='M7.77295 16.3566L23.6563 0H32V6.88383C32 6.88383 31.8262 9.17836 30.6591 10.4057L19.7824 22H13.6938L7.77295 16.3566Z'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<Box sx={{ my: 6 }}>
|
||||||
|
<Typography sx={{ mb: 1.5, fontWeight: 500, fontSize: '1.625rem', lineHeight: 1.385 }}>
|
||||||
|
Forgot Password? 🔒
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: 'text.secondary' }}>
|
||||||
|
Enter your email and we′ll send you instructions to reset your password
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<form noValidate autoComplete='off' onSubmit={e => e.preventDefault()}>
|
||||||
|
<TextField autoFocus type='email' label='Email' sx={{ display: 'flex', mb: 4 }} />
|
||||||
|
<Button fullWidth size='large' type='submit' variant='contained' sx={{ mb: 4 }}>
|
||||||
|
Send reset link
|
||||||
|
</Button>
|
||||||
|
<Typography sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', '& svg': { mr: 1 } }}>
|
||||||
|
<LinkStyled href='/login'>
|
||||||
|
<Icon fontSize='1.25rem' icon='tabler:chevron-left' />
|
||||||
|
<span>Back to login</span>
|
||||||
|
</LinkStyled>
|
||||||
|
</Typography>
|
||||||
|
</form>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</RightWrapper>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ForgotPassword.getLayout = (page: ReactNode) => <BlankLayout>{page}</BlankLayout>
|
||||||
|
|
||||||
|
ForgotPassword.guestGuard = true
|
||||||
|
|
||||||
|
export default ForgotPassword
|
||||||
38
app/home/index.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import Card from '@mui/material/Card'
|
||||||
|
import Grid from '@mui/material/Grid'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import CardHeader from '@mui/material/CardHeader'
|
||||||
|
import CardContent from '@mui/material/CardContent'
|
||||||
|
|
||||||
|
const Home = () => {
|
||||||
|
return (
|
||||||
|
<Grid container spacing={6}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Card>
|
||||||
|
<CardHeader title='Kick start your project 🚀'></CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Typography sx={{ mb: 2 }}>All the best for your new project.</Typography>
|
||||||
|
<Typography>
|
||||||
|
Please make sure to read our Template Documentation to understand where to go from here and how to use our
|
||||||
|
template.
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Card>
|
||||||
|
<CardHeader title='ACL and JWT 🔒'></CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Typography sx={{ mb: 2 }}>
|
||||||
|
Access Control (ACL) and Authentication (JWT) are the two main security features of our template and are implemented in the starter-kit as well.
|
||||||
|
</Typography>
|
||||||
|
<Typography>Please read our Authentication and ACL Documentations to get more out of them.</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home
|
||||||
38
app/home/page.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import Card from '@mui/material/Card'
|
||||||
|
import Grid from '@mui/material/Grid'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import CardHeader from '@mui/material/CardHeader'
|
||||||
|
import CardContent from '@mui/material/CardContent'
|
||||||
|
|
||||||
|
const Home = () => {
|
||||||
|
return (
|
||||||
|
<Grid container spacing={6}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Card>
|
||||||
|
<CardHeader title='Kick start your project 🚀'></CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Typography sx={{ mb: 2 }}>All the best for your new project.</Typography>
|
||||||
|
<Typography>
|
||||||
|
Please make sure to read our Template Documentation to understand where to go from here and how to use our
|
||||||
|
template.
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Card>
|
||||||
|
<CardHeader title='ACL and JWT 🔒'></CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Typography sx={{ mb: 2 }}>
|
||||||
|
Access Control (ACL) and Authentication (JWT) are the two main security features of our template and are implemented in the starter-kit as well.
|
||||||
|
</Typography>
|
||||||
|
<Typography>Please read our Authentication and ACL Documentations to get more out of them.</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home
|
||||||
98
app/layouts/UserLayout.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
// ** MUI Imports
|
||||||
|
import { Theme } from '@mui/material/styles'
|
||||||
|
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||||
|
|
||||||
|
// ** Layout Imports
|
||||||
|
// !Do not remove this Layout import
|
||||||
|
import Layout from '../../src/@core/layouts/Layout'
|
||||||
|
|
||||||
|
// ** Navigation Imports
|
||||||
|
import VerticalNavItems from '../../src/navigation/vertical'
|
||||||
|
import HorizontalNavItems from '../../src/navigation/horizontal'
|
||||||
|
|
||||||
|
// ** Component Import
|
||||||
|
// Uncomment the below line (according to the layout type) when using server-side menu
|
||||||
|
// import ServerSideVerticalNavItems from './components/vertical/ServerSideNavItems'
|
||||||
|
// import ServerSideHorizontalNavItems from './components/horizontal/ServerSideNavItems'
|
||||||
|
|
||||||
|
import VerticalAppBarContent from './components/vertical/AppBarContent'
|
||||||
|
import HorizontalAppBarContent from './components/horizontal/AppBarContent'
|
||||||
|
|
||||||
|
// ** Hook Import
|
||||||
|
import { useSettings } from '../../src/@core/hooks/useSettings'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode
|
||||||
|
contentHeightFixed?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserLayout = ({ children, contentHeightFixed }: Props) => {
|
||||||
|
// ** Hooks
|
||||||
|
const { settings, saveSettings } = useSettings()
|
||||||
|
|
||||||
|
// ** Vars for server side navigation
|
||||||
|
// const { menuItems: verticalMenuItems } = ServerSideVerticalNavItems()
|
||||||
|
// const { menuItems: horizontalMenuItems } = ServerSideHorizontalNavItems()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The below variable will hide the current layout menu at given screen size.
|
||||||
|
* The menu will be accessible from the Hamburger icon only (Vertical Overlay Menu).
|
||||||
|
* You can change the screen size from which you want to hide the current layout menu.
|
||||||
|
* Please refer useMediaQuery() hook: https://mui.com/material-ui/react-use-media-query/,
|
||||||
|
* to know more about what values can be passed to this hook.
|
||||||
|
* ! Do not change this value unless you know what you are doing. It can break the template.
|
||||||
|
*/
|
||||||
|
const hidden = useMediaQuery((theme: Theme) => theme.breakpoints.down('lg'))
|
||||||
|
|
||||||
|
if (hidden && settings.layout === 'horizontal') {
|
||||||
|
settings.layout = 'vertical'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout
|
||||||
|
hidden={hidden}
|
||||||
|
settings={settings}
|
||||||
|
saveSettings={saveSettings}
|
||||||
|
contentHeightFixed={contentHeightFixed}
|
||||||
|
verticalLayoutProps={{
|
||||||
|
navMenu: {
|
||||||
|
navItems: VerticalNavItems()
|
||||||
|
|
||||||
|
// Uncomment the below line when using server-side menu in vertical layout and comment the above line
|
||||||
|
// navItems: verticalMenuItems
|
||||||
|
},
|
||||||
|
appBar: {
|
||||||
|
content: props => (
|
||||||
|
<VerticalAppBarContent
|
||||||
|
hidden={hidden}
|
||||||
|
settings={settings}
|
||||||
|
saveSettings={saveSettings}
|
||||||
|
toggleNavVisibility={props.toggleNavVisibility}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...(settings.layout === 'horizontal' && {
|
||||||
|
horizontalLayoutProps: {
|
||||||
|
navMenu: {
|
||||||
|
navItems: HorizontalNavItems()
|
||||||
|
|
||||||
|
// Uncomment the below line when using server-side menu in horizontal layout and comment the above line
|
||||||
|
// navItems: horizontalMenuItems
|
||||||
|
},
|
||||||
|
appBar: {
|
||||||
|
content: () => <HorizontalAppBarContent settings={settings} saveSettings={saveSettings} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserLayout
|
||||||
185
app/layouts/UserThemeOptions.ts
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import { ThemeOptions } from '@mui/system'
|
||||||
|
|
||||||
|
// ** To use core palette, uncomment the below import
|
||||||
|
// import { PaletteMode } from '@mui/material'
|
||||||
|
|
||||||
|
// ** To use core palette, uncomment the below import
|
||||||
|
// import corePalette from 'src/@core/theme/palette'
|
||||||
|
|
||||||
|
// ** To use mode (light/dark/semi-dark), skin(default/bordered), direction(ltr/rtl), etc. for conditional styles, uncomment below line
|
||||||
|
// import { useSettings } from 'src/@core/hooks/useSettings'
|
||||||
|
|
||||||
|
const UserThemeOptions = (): ThemeOptions => {
|
||||||
|
// ** To use mode (light/dark/semi-dark), skin(default/bordered), direction(ltr/rtl), etc. for conditional styles, uncomment below line
|
||||||
|
// const { settings } = useSettings()
|
||||||
|
|
||||||
|
// ** To use mode (light/dark/semi-dark), skin(default/bordered), direction(ltr/rtl), etc. for conditional styles, uncomment below line
|
||||||
|
// const { mode, skin } = settings
|
||||||
|
|
||||||
|
// ** To use core palette, uncomment the below line
|
||||||
|
// const palette = corePalette(mode as PaletteMode, skin)
|
||||||
|
|
||||||
|
return {
|
||||||
|
/*
|
||||||
|
palette:{
|
||||||
|
primary: {
|
||||||
|
light: '#8479F2',
|
||||||
|
main: '#7367F0',
|
||||||
|
dark: '#655BD3',
|
||||||
|
contrastText: '#FFF'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
breakpoints: {
|
||||||
|
values: {
|
||||||
|
xs: 0,
|
||||||
|
sm: 768,
|
||||||
|
md: 992,
|
||||||
|
lg: 1200,
|
||||||
|
xl: 1920
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
MuiButton: {
|
||||||
|
defaultProps: {
|
||||||
|
disableElevation: true
|
||||||
|
},
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
textTransform: 'none'
|
||||||
|
},
|
||||||
|
sizeSmall: {
|
||||||
|
padding: '6px 16px'
|
||||||
|
},
|
||||||
|
sizeMedium: {
|
||||||
|
padding: '8px 20px'
|
||||||
|
},
|
||||||
|
sizeLarge: {
|
||||||
|
padding: '11px 24px'
|
||||||
|
},
|
||||||
|
textSizeSmall: {
|
||||||
|
padding: '7px 12px'
|
||||||
|
},
|
||||||
|
textSizeMedium: {
|
||||||
|
padding: '9px 16px'
|
||||||
|
},
|
||||||
|
textSizeLarge: {
|
||||||
|
padding: '12px 16px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiCardActions: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
padding: '16px 24px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiCardContent: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
padding: '32px 24px',
|
||||||
|
'&:last-child': {
|
||||||
|
paddingBottom: '32px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MuiCssBaseline: {
|
||||||
|
styleOverrides: {
|
||||||
|
'*': {
|
||||||
|
boxSizing: 'border-box'
|
||||||
|
},
|
||||||
|
html: {
|
||||||
|
MozOsxFontSmoothing: 'grayscale',
|
||||||
|
WebkitFontSmoothing: 'antialiased',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
minHeight: '100%',
|
||||||
|
width: '100%'
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
display: 'flex',
|
||||||
|
flex: '1 1 auto',
|
||||||
|
flexDirection: 'column',
|
||||||
|
minHeight: '100%',
|
||||||
|
width: '100%'
|
||||||
|
},
|
||||||
|
'#__next': {
|
||||||
|
display: 'flex',
|
||||||
|
flex: '1 1 auto',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shape: {
|
||||||
|
borderRadius: 8
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
fontFamily:
|
||||||
|
'"Montserrat", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"',
|
||||||
|
},
|
||||||
|
shadows: mode === 'light' ? [
|
||||||
|
'none',
|
||||||
|
'0px 2px 4px 1px rgba(51, 48, 60, 0.03), 0px 3px 4px 0px rgba(51, 48, 60, 0.02), 0px 1px 3px 2px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 3px 5px 2px rgba(51, 48, 60, 0.03), 0px 3px 5px 0px rgba(51, 48, 60, 0.02), 0px 1px 4px 2px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 3px 6px 2px rgba(51, 48, 60, 0.03), 0px 4px 6px 0px rgba(51, 48, 60, 0.02), 0px 1px 4px 2px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 2px 7px 1px rgba(51, 48, 60, 0.03), 0px 4px 7px 0px rgba(51, 48, 60, 0.02), 0px 1px 4px 2px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 3px 8px 1px rgba(51, 48, 60, 0.03), 0px 6px 8px 0px rgba(51, 48, 60, 0.02), 0px 1px 5px 4px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 3px 9px 1px rgba(51, 48, 60, 0.03), 0px 8px 9px 0px rgba(51, 48, 60, 0.02), 0px 1px 6px 4px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 4px 10px 2px rgba(51, 48, 60, 0.03), 0px 9px 10px 1px rgba(51, 48, 60, 0.02), 0px 2px 7px 4px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 5px 11px 3px rgba(51, 48, 60, 0.03), 0px 8px 11px 1px rgba(51, 48, 60, 0.02), 0px 3px 8px 4px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 5px 12px 3px rgba(51, 48, 60, 0.03), 0px 9px 12px 1px rgba(51, 48, 60, 0.02), 0px 3px 9px 5px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 6px 13px 3px rgba(51, 48, 60, 0.03), 0px 10px 13px 1px rgba(51, 48, 60, 0.02), 0px 4px 10px 5px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 6px 14px 4px rgba(51, 48, 60, 0.03), 0px 11px 14px 1px rgba(51, 48, 60, 0.02), 0px 4px 11px 5px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 7px 15px 4px rgba(51, 48, 60, 0.03), 0px 12px 15px 2px rgba(51, 48, 60, 0.02), 0px 5px 12px 5px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 7px 16px 4px rgba(51, 48, 60, 0.03), 0px 13px 16px 2px rgba(51, 48, 60, 0.02), 0px 5px 13px 6px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 7px 17px 4px rgba(51, 48, 60, 0.03), 0px 14px 17px 2px rgba(51, 48, 60, 0.02), 0px 5px 14px 6px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 8px 18px 5px rgba(51, 48, 60, 0.03), 0px 15px 18px 2px rgba(51, 48, 60, 0.02), 0px 6px 15px 6px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 8px 19px 5px rgba(51, 48, 60, 0.03), 0px 16px 19px 2px rgba(51, 48, 60, 0.02), 0px 6px 16px 6px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 8px 20px 5px rgba(51, 48, 60, 0.03), 0px 17px 20px 2px rgba(51, 48, 60, 0.02), 0px 6px 17px 7px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 9px 21px 5px rgba(51, 48, 60, 0.03), 0px 18px 21px 2px rgba(51, 48, 60, 0.02), 0px 7px 18px 7px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 9px 22px 6px rgba(51, 48, 60, 0.03), 0px 19px 22px 2px rgba(51, 48, 60, 0.02), 0px 7px 19px 7px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 10px 23px 6px rgba(51, 48, 60, 0.03), 0px 20px 23px 3px rgba(51, 48, 60, 0.02), 0px 8px 20px 7px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 10px 24px 6px rgba(51, 48, 60, 0.03), 0px 21px 24px 3px rgba(51, 48, 60, 0.02), 0px 8px 21px 7px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 10px 25px 6px rgba(51, 48, 60, 0.03), 0px 22px 25px 3px rgba(51, 48, 60, 0.02), 0px 8px 22px 7px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 11px 26px 7px rgba(51, 48, 60, 0.03), 0px 23px 26px 3px rgba(51, 48, 60, 0.02), 0px 9px 23px 7px rgba(51, 48, 60, 0.01)',
|
||||||
|
'0px 11px 27px 7px rgba(51, 48, 60, 0.03), 0px 24px 27px 3px rgba(51, 48, 60, 0.02), 0px 9px 24px 7px rgba(51, 48, 60, 0.01)'
|
||||||
|
] : [
|
||||||
|
'none',
|
||||||
|
'0px 2px 4px 1px rgba(12, 16, 27, 0.15), 0px 3px 4px 0px rgba(12, 16, 27, 0.1), 0px 1px 3px 2px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 3px 5px 2px rgba(12, 16, 27, 0.15), 0px 3px 5px 0px rgba(12, 16, 27, 0.1), 0px 1px 4px 2px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 3px 6px 2px rgba(12, 16, 27, 0.15), 0px 4px 6px 0px rgba(12, 16, 27, 0.1), 0px 1px 4px 2px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 2px 7px 1px rgba(12, 16, 27, 0.15), 0px 4px 7px 0px rgba(12, 16, 27, 0.1), 0px 1px 4px 2px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 3px 8px 1px rgba(12, 16, 27, 0.15), 0px 6px 8px 0px rgba(12, 16, 27, 0.1), 0px 1px 5px 4px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 3px 9px 1px rgba(12, 16, 27, 0.15), 0px 8px 9px 0px rgba(12, 16, 27, 0.1), 0px 1px 6px 4px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 4px 10px 2px rgba(12, 16, 27, 0.15), 0px 9px 10px 1px rgba(12, 16, 27, 0.1), 0px 2px 7px 4px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 5px 11px 3px rgba(12, 16, 27, 0.15), 0px 8px 11px 1px rgba(12, 16, 27, 0.1), 0px 3px 8px 4px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 5px 12px 3px rgba(12, 16, 27, 0.15), 0px 9px 12px 1px rgba(12, 16, 27, 0.1), 0px 3px 9px 5px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 6px 13px 3px rgba(12, 16, 27, 0.15), 0px 10px 13px 1px rgba(12, 16, 27, 0.1), 0px 4px 10px 5px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 6px 14px 4px rgba(12, 16, 27, 0.15), 0px 11px 14px 1px rgba(12, 16, 27, 0.1), 0px 4px 11px 5px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 7px 15px 4px rgba(12, 16, 27, 0.15), 0px 12px 15px 2px rgba(12, 16, 27, 0.1), 0px 5px 12px 5px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 7px 16px 4px rgba(12, 16, 27, 0.15), 0px 13px 16px 2px rgba(12, 16, 27, 0.1), 0px 5px 13px 6px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 7px 17px 4px rgba(12, 16, 27, 0.15), 0px 14px 17px 2px rgba(12, 16, 27, 0.1), 0px 5px 14px 6px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 8px 18px 5px rgba(12, 16, 27, 0.15), 0px 15px 18px 2px rgba(12, 16, 27, 0.1), 0px 6px 15px 6px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 8px 19px 5px rgba(12, 16, 27, 0.15), 0px 16px 19px 2px rgba(12, 16, 27, 0.1), 0px 6px 16px 6px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 8px 20px 5px rgba(12, 16, 27, 0.15), 0px 17px 20px 2px rgba(12, 16, 27, 0.1), 0px 6px 17px 7px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 9px 21px 5px rgba(12, 16, 27, 0.15), 0px 18px 21px 2px rgba(12, 16, 27, 0.1), 0px 7px 18px 7px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 9px 22px 6px rgba(12, 16, 27, 0.15), 0px 19px 22px 2px rgba(12, 16, 27, 0.1), 0px 7px 19px 7px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 10px 23px 6px rgba(12, 16, 27, 0.15), 0px 20px 23px 3px rgba(12, 16, 27, 0.1), 0px 8px 20px 7px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 10px 24px 6px rgba(12, 16, 27, 0.15), 0px 21px 24px 3px rgba(12, 16, 27, 0.1), 0px 8px 21px 7px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 10px 25px 6px rgba(12, 16, 27, 0.15), 0px 22px 25px 3px rgba(12, 16, 27, 0.1), 0px 8px 22px 7px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 11px 26px 7px rgba(12, 16, 27, 0.15), 0px 23px 26px 3px rgba(12, 16, 27, 0.1), 0px 9px 23px 7px rgba(12, 16, 27, 0.08)',
|
||||||
|
'0px 11px 27px 7px rgba(12, 16, 27, 0.15), 0px 24px 27px 3px rgba(12, 16, 27, 0.1), 0px 9px 24px 7px rgba(12, 16, 27, 0.08)'
|
||||||
|
],
|
||||||
|
zIndex: {
|
||||||
|
appBar: 1200,
|
||||||
|
drawer: 1100
|
||||||
|
} */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserThemeOptions
|
||||||
40
app/layouts/components/Direction.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { useEffect, ReactNode } from 'react'
|
||||||
|
|
||||||
|
// ** MUI Imports
|
||||||
|
import { Direction } from '@mui/material'
|
||||||
|
|
||||||
|
// ** Emotion Imports
|
||||||
|
import createCache from '@emotion/cache'
|
||||||
|
import { CacheProvider } from '@emotion/react'
|
||||||
|
|
||||||
|
// ** RTL Plugin
|
||||||
|
import stylisRTLPlugin from 'stylis-plugin-rtl'
|
||||||
|
|
||||||
|
interface DirectionProps {
|
||||||
|
children: ReactNode
|
||||||
|
direction: Direction
|
||||||
|
}
|
||||||
|
|
||||||
|
const styleCache = () =>
|
||||||
|
createCache({
|
||||||
|
key: 'rtl',
|
||||||
|
prepend: true,
|
||||||
|
stylisPlugins: [stylisRTLPlugin]
|
||||||
|
})
|
||||||
|
|
||||||
|
const Direction = (props: DirectionProps) => {
|
||||||
|
const { children, direction } = props
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.dir = direction
|
||||||
|
}, [direction])
|
||||||
|
|
||||||
|
if (direction === 'rtl') {
|
||||||
|
return <CacheProvider value={styleCache()}>{children}</CacheProvider>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Direction
|
||||||
15
app/layouts/components/Translations.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Translations = ({ text }: Props) => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return <>{text}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Translations
|
||||||
11
app/layouts/components/UserIcon.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// ** Type Import
|
||||||
|
import { IconProps } from '@iconify/react'
|
||||||
|
|
||||||
|
// ** Custom Icon Import
|
||||||
|
import Icon from 'src/@core/components/icon'
|
||||||
|
|
||||||
|
const UserIcon = ({ icon, ...rest }: IconProps) => {
|
||||||
|
return <Icon icon={icon} {...rest} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserIcon
|
||||||
7
app/layouts/components/acl/Can.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { createContext } from 'react'
|
||||||
|
import { AnyAbility } from '@casl/ability'
|
||||||
|
import { createContextualCan } from '@casl/react'
|
||||||
|
|
||||||
|
export const AbilityContext = createContext<AnyAbility>(undefined!)
|
||||||
|
|
||||||
|
export default createContextualCan(AbilityContext.Consumer)
|
||||||
45
app/layouts/components/acl/CanViewNavGroup.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { ReactNode, useContext } from 'react'
|
||||||
|
|
||||||
|
// ** Component Imports
|
||||||
|
import { AbilityContext } from 'src/layouts/components/acl/Can'
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { NavGroup, NavLink } from 'src/@core/layouts/types'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
navGroup?: NavGroup
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const CanViewNavGroup = (props: Props) => {
|
||||||
|
// ** Props
|
||||||
|
const { children, navGroup } = props
|
||||||
|
|
||||||
|
// ** Hook
|
||||||
|
const ability = useContext(AbilityContext)
|
||||||
|
|
||||||
|
const checkForVisibleChild = (arr: NavLink[] | NavGroup[]): boolean => {
|
||||||
|
return arr.some((i: NavGroup) => {
|
||||||
|
if (i.children) {
|
||||||
|
return checkForVisibleChild(i.children)
|
||||||
|
} else {
|
||||||
|
return ability?.can(i.action, i.subject)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const canViewMenuGroup = (item: NavGroup) => {
|
||||||
|
const hasAnyVisibleChild = item.children && checkForVisibleChild(item.children)
|
||||||
|
|
||||||
|
if (!(item.action && item.subject)) {
|
||||||
|
return hasAnyVisibleChild
|
||||||
|
}
|
||||||
|
|
||||||
|
return ability && ability.can(item.action, item.subject) && hasAnyVisibleChild
|
||||||
|
}
|
||||||
|
|
||||||
|
return navGroup && canViewMenuGroup(navGroup) ? <>{children}</> : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CanViewNavGroup
|
||||||
25
app/layouts/components/acl/CanViewNavLink.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { ReactNode, useContext } from 'react'
|
||||||
|
|
||||||
|
// ** Component Imports
|
||||||
|
import { AbilityContext } from 'src/layouts/components/acl/Can'
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { NavLink } from 'src/@core/layouts/types'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
navLink?: NavLink
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const CanViewNavLink = (props: Props) => {
|
||||||
|
// ** Props
|
||||||
|
const { children, navLink } = props
|
||||||
|
|
||||||
|
// ** Hook
|
||||||
|
const ability = useContext(AbilityContext)
|
||||||
|
|
||||||
|
return ability && ability.can(navLink?.action, navLink?.subject) ? <>{children}</> : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CanViewNavLink
|
||||||
25
app/layouts/components/acl/CanViewNavSectionTitle.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { ReactNode, useContext } from 'react'
|
||||||
|
|
||||||
|
// ** Component Imports
|
||||||
|
import { AbilityContext } from 'src/layouts/components/acl/Can'
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { NavSectionTitle } from 'src/@core/layouts/types'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode
|
||||||
|
navTitle?: NavSectionTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
const CanViewNavSectionTitle = (props: Props) => {
|
||||||
|
// ** Props
|
||||||
|
const { children, navTitle } = props
|
||||||
|
|
||||||
|
// ** Hook
|
||||||
|
const ability = useContext(AbilityContext)
|
||||||
|
|
||||||
|
return ability && ability.can(navTitle?.action, navTitle?.subject) ? <>{children}</> : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CanViewNavSectionTitle
|
||||||
27
app/layouts/components/horizontal/AppBarContent.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
|
||||||
|
// ** Type Import
|
||||||
|
import { Settings } from '../../../../src/@core/context/settingsContext'
|
||||||
|
|
||||||
|
// ** Components
|
||||||
|
import ModeToggler from '../../../../src/@core/layouts/components/shared-components/ModeToggler'
|
||||||
|
import UserDropdown from '../../../../src/@core/layouts/components/shared-components/UserDropdown'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
settings: Settings
|
||||||
|
saveSettings: (values: Settings) => void
|
||||||
|
}
|
||||||
|
const AppBarContent = (props: Props) => {
|
||||||
|
// ** Props
|
||||||
|
const { settings, saveSettings } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<ModeToggler settings={settings} saveSettings={saveSettings} />
|
||||||
|
<UserDropdown settings={settings} />
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppBarContent
|
||||||
25
app/layouts/components/horizontal/ServerSideNavItems.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
// ** Axios Import
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
// ** Type Import
|
||||||
|
import { HorizontalNavItemsType } from '../../../../src/@core/layouts/types'
|
||||||
|
|
||||||
|
const ServerSideNavItems = () => {
|
||||||
|
// ** State
|
||||||
|
const [menuItems, setMenuItems] = useState<HorizontalNavItemsType>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
axios.get('/api/horizontal-nav/data').then(response => {
|
||||||
|
const menuArray = response.data
|
||||||
|
|
||||||
|
setMenuItems(menuArray)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return { menuItems }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ServerSideNavItems
|
||||||
45
app/layouts/components/vertical/AppBarContent.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import IconButton from '@mui/material/IconButton'
|
||||||
|
|
||||||
|
// ** Icon Imports
|
||||||
|
import Icon from '../../../../src/@core/components/icon'
|
||||||
|
|
||||||
|
|
||||||
|
// ** Type Import
|
||||||
|
import { Settings } from '../../../../src/@core/context/settingsContext'
|
||||||
|
|
||||||
|
// ** Components
|
||||||
|
import ModeToggler from '../../../../src/@core/layouts/components/shared-components/ModeToggler'
|
||||||
|
import UserDropdown from '../../../../src/@core/layouts/components/shared-components/UserDropdown'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
hidden: boolean
|
||||||
|
settings: Settings
|
||||||
|
toggleNavVisibility: () => void
|
||||||
|
saveSettings: (values: Settings) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppBarContent = (props: Props) => {
|
||||||
|
// ** Props
|
||||||
|
const { hidden, settings, saveSettings, toggleNavVisibility } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<Box className='actions-left' sx={{ mr: 2, display: 'flex', alignItems: 'center' }}>
|
||||||
|
{hidden ? (
|
||||||
|
<IconButton color='inherit' sx={{ ml: -2.75 }} onClick={toggleNavVisibility}>
|
||||||
|
<Icon fontSize='1.5rem' icon='tabler:menu-2' />
|
||||||
|
</IconButton>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<ModeToggler settings={settings} saveSettings={saveSettings} />
|
||||||
|
</Box>
|
||||||
|
<Box className='actions-right' sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<UserDropdown settings={settings} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppBarContent
|
||||||
25
app/layouts/components/vertical/ServerSideNavItems.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
// ** Axios Import
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
// ** Type Import
|
||||||
|
import { VerticalNavItemsType } from '../../../../src/@core/layouts/types'
|
||||||
|
|
||||||
|
const ServerSideNavItems = () => {
|
||||||
|
// ** State
|
||||||
|
const [menuItems, setMenuItems] = useState<VerticalNavItemsType>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
axios.get('/api/vertical-nav/data').then(response => {
|
||||||
|
const menuArray = response.data
|
||||||
|
|
||||||
|
setMenuItems(menuArray)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return { menuItems }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ServerSideNavItems
|
||||||
352
app/login/components/login.tsx
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
"use client"
|
||||||
|
// ** React Imports
|
||||||
|
import { useState, ReactNode, MouseEvent } from 'react'
|
||||||
|
|
||||||
|
// ** Next Imports
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
// ** MUI Components
|
||||||
|
import Alert from '@mui/material/Alert'
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import Divider from '@mui/material/Divider'
|
||||||
|
import Checkbox from '@mui/material/Checkbox'
|
||||||
|
import TextField from '@mui/material/TextField'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import InputLabel from '@mui/material/InputLabel'
|
||||||
|
import IconButton from '@mui/material/IconButton'
|
||||||
|
import Box, { BoxProps } from '@mui/material/Box'
|
||||||
|
import FormControl from '@mui/material/FormControl'
|
||||||
|
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||||
|
import OutlinedInput from '@mui/material/OutlinedInput'
|
||||||
|
import { styled, useTheme } from '@mui/material/styles'
|
||||||
|
import FormHelperText from '@mui/material/FormHelperText'
|
||||||
|
import InputAdornment from '@mui/material/InputAdornment'
|
||||||
|
import MuiFormControlLabel, { FormControlLabelProps } from '@mui/material/FormControlLabel'
|
||||||
|
|
||||||
|
// ** Icon Imports
|
||||||
|
import Icon from '../../../src/@core/components/icon'
|
||||||
|
|
||||||
|
// ** Third Party Imports
|
||||||
|
import * as yup from 'yup'
|
||||||
|
import { useForm, Controller } from 'react-hook-form'
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup'
|
||||||
|
|
||||||
|
// ** Hooks
|
||||||
|
import { useAuth } from '../../../src/hooks/useAuth'
|
||||||
|
import useBgColor from '../../../src/@core/hooks/useBgColor'
|
||||||
|
import { useSettings } from '../../../src/@core/hooks/useSettings'
|
||||||
|
|
||||||
|
// ** Configs
|
||||||
|
import themeConfig from '../../../src/configs/themeConfig'
|
||||||
|
|
||||||
|
// ** Layout Import
|
||||||
|
import BlankLayout from '../../../src/@core/layouts/BlankLayout'
|
||||||
|
|
||||||
|
// ** Demo Imports
|
||||||
|
import FooterIllustrationsV2 from '../../../src/views/pages/auth/FooterIllustrationsV2'
|
||||||
|
|
||||||
|
// ** Styled Components
|
||||||
|
const LoginIllustration = styled('img')(({ theme }) => ({
|
||||||
|
zIndex: 2,
|
||||||
|
maxHeight: 680,
|
||||||
|
marginTop: theme.spacing(12),
|
||||||
|
marginBottom: theme.spacing(12),
|
||||||
|
[theme.breakpoints.down(1540)]: {
|
||||||
|
maxHeight: 550
|
||||||
|
},
|
||||||
|
[theme.breakpoints.down('lg')]: {
|
||||||
|
maxHeight: 500
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const RightWrapper = styled(Box)<BoxProps>(({ theme }) => ({
|
||||||
|
width: '100%',
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
maxWidth: 450
|
||||||
|
},
|
||||||
|
[theme.breakpoints.up('lg')]: {
|
||||||
|
maxWidth: 600
|
||||||
|
},
|
||||||
|
[theme.breakpoints.up('xl')]: {
|
||||||
|
maxWidth: 750
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const LinkStyled = styled(Link)(({ theme }) => ({
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: theme.palette.primary.main
|
||||||
|
}))
|
||||||
|
|
||||||
|
const FormControlLabel = styled(MuiFormControlLabel)<FormControlLabelProps>(({ theme }) => ({
|
||||||
|
'& .MuiFormControlLabel-label': {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
color: theme.palette.text.secondary
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const schema = yup.object().shape({
|
||||||
|
email: yup.string().email().required(),
|
||||||
|
password: yup.string().min(5).required()
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
password: 'admin',
|
||||||
|
email: 'admin@vuexy.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormData {
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginPage = () => {
|
||||||
|
const [rememberMe, setRememberMe] = useState<boolean>(true)
|
||||||
|
const [showPassword, setShowPassword] = useState<boolean>(false)
|
||||||
|
|
||||||
|
// ** Hooks
|
||||||
|
const auth = useAuth()
|
||||||
|
const theme = useTheme()
|
||||||
|
const bgColors = useBgColor()
|
||||||
|
const { settings } = useSettings()
|
||||||
|
const hidden = useMediaQuery(theme.breakpoints.down('md'))
|
||||||
|
|
||||||
|
// ** Vars
|
||||||
|
const { skin } = settings
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
setError,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors }
|
||||||
|
} = useForm({
|
||||||
|
defaultValues,
|
||||||
|
mode: 'onBlur',
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = (data: FormData) => {
|
||||||
|
const { email, password } = data
|
||||||
|
auth.login({ email, password, rememberMe }, () => {
|
||||||
|
setError('email', {
|
||||||
|
type: 'manual',
|
||||||
|
message: 'Email or Password is invalid'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageSource = skin === 'bordered' ? 'auth-v2-login-illustration-bordered' : 'auth-v2-login-illustration'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className='content-right' sx={{ backgroundColor: 'background.paper' }}>
|
||||||
|
{!hidden ? (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
position: 'relative',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: '20px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: 'customColors.bodyBg',
|
||||||
|
margin: theme => theme.spacing(8, 0, 8, 8)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LoginIllustration alt='login-illustration' src={`/images/pages/${imageSource}-${theme.palette.mode}.png`} />
|
||||||
|
<FooterIllustrationsV2 />
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
<RightWrapper>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: [6, 12],
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ width: '100%', maxWidth: 400 }}>
|
||||||
|
<svg width={34} height={23.375} viewBox='0 0 32 22' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||||
|
<path
|
||||||
|
fillRule='evenodd'
|
||||||
|
clipRule='evenodd'
|
||||||
|
fill={theme.palette.primary.main}
|
||||||
|
d='M0.00172773 0V6.85398C0.00172773 6.85398 -0.133178 9.01207 1.98092 10.8388L13.6912 21.9964L19.7809 21.9181L18.8042 9.88248L16.4951 7.17289L9.23799 0H0.00172773Z'
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill='#161616'
|
||||||
|
opacity={0.06}
|
||||||
|
fillRule='evenodd'
|
||||||
|
clipRule='evenodd'
|
||||||
|
d='M7.69824 16.4364L12.5199 3.23696L16.5541 7.25596L7.69824 16.4364Z'
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill='#161616'
|
||||||
|
opacity={0.06}
|
||||||
|
fillRule='evenodd'
|
||||||
|
clipRule='evenodd'
|
||||||
|
d='M8.07751 15.9175L13.9419 4.63989L16.5849 7.28475L8.07751 15.9175Z'
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule='evenodd'
|
||||||
|
clipRule='evenodd'
|
||||||
|
fill={theme.palette.primary.main}
|
||||||
|
d='M7.77295 16.3566L23.6563 0H32V6.88383C32 6.88383 31.8262 9.17836 30.6591 10.4057L19.7824 22H13.6938L7.77295 16.3566Z'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<Box sx={{ my: 6 }}>
|
||||||
|
<Typography sx={{ mb: 1.5, fontWeight: 500, fontSize: '1.625rem', lineHeight: 1.385 }}>
|
||||||
|
{`Welcome to ${themeConfig.templateName}! 👋🏻`}
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: 'text.secondary' }}>
|
||||||
|
Please sign-in to your account and start the adventure
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Alert icon={false} sx={{ py: 3, mb: 6, ...bgColors.primaryLight, '& .MuiAlert-message': { p: 0 } }}>
|
||||||
|
<Typography variant='body2' sx={{ mb: 2, color: 'primary.main' }}>
|
||||||
|
Admin: <strong>admin@vuexy.com</strong> / Pass: <strong>admin</strong>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body2' sx={{ color: 'primary.main' }}>
|
||||||
|
Client: <strong>client@vuexy.com</strong> / Pass: <strong>client</strong>
|
||||||
|
</Typography>
|
||||||
|
</Alert>
|
||||||
|
<form noValidate autoComplete='off' onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<FormControl fullWidth sx={{ mb: 4 }}>
|
||||||
|
<Controller
|
||||||
|
name='email'
|
||||||
|
control={control}
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field: { value, onChange, onBlur } }) => (
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
label='Email'
|
||||||
|
value={value}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onChange={onChange}
|
||||||
|
error={Boolean(errors.email)}
|
||||||
|
placeholder='admin@vuexy.com'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.email && <FormHelperText sx={{ color: 'error.main' }}>{errors.email.message}</FormHelperText>}
|
||||||
|
</FormControl>
|
||||||
|
<FormControl fullWidth sx={{ mb: 1.5 }}>
|
||||||
|
<InputLabel htmlFor='auth-login-v2-password' error={Boolean(errors.password)}>
|
||||||
|
Password
|
||||||
|
</InputLabel>
|
||||||
|
<Controller
|
||||||
|
name='password'
|
||||||
|
control={control}
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field: { value, onChange, onBlur } }) => (
|
||||||
|
<OutlinedInput
|
||||||
|
value={value}
|
||||||
|
onBlur={onBlur}
|
||||||
|
label='Password'
|
||||||
|
onChange={onChange}
|
||||||
|
id='auth-login-v2-password'
|
||||||
|
error={Boolean(errors.password)}
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
endAdornment={
|
||||||
|
<InputAdornment position='end'>
|
||||||
|
<IconButton
|
||||||
|
edge='end'
|
||||||
|
onMouseDown={e => e.preventDefault()}
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
>
|
||||||
|
<Icon icon={showPassword ? 'tabler:eye' : 'tabler:eye-off'} fontSize={20} />
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.password && (
|
||||||
|
<FormHelperText sx={{ color: 'error.main' }} id=''>
|
||||||
|
{errors.password.message}
|
||||||
|
</FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mb: 1.75,
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControlLabel
|
||||||
|
label='Remember Me'
|
||||||
|
control={<Checkbox checked={rememberMe} onChange={e => setRememberMe(e.target.checked)} />}
|
||||||
|
/>
|
||||||
|
<LinkStyled href='/forgot-password'>Forgot Password?</LinkStyled>
|
||||||
|
</Box>
|
||||||
|
<Button fullWidth size='large' type='submit' variant='contained' sx={{ mb: 4 }}>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', justifyContent: 'center' }}>
|
||||||
|
<Typography sx={{ color: 'text.secondary', mr: 2 }}>New on our platform?</Typography>
|
||||||
|
<Typography variant='body2'>
|
||||||
|
<LinkStyled href='/register' sx={{ fontSize: '1rem' }}>
|
||||||
|
Create an account
|
||||||
|
</LinkStyled>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider
|
||||||
|
sx={{
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
color: 'text.disabled',
|
||||||
|
'& .MuiDivider-wrapper': { px: 6 },
|
||||||
|
my: theme => `${theme.spacing(6)} !important`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
or
|
||||||
|
</Divider>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<IconButton
|
||||||
|
href='/'
|
||||||
|
component={Link}
|
||||||
|
sx={{ color: '#497ce2' }}
|
||||||
|
onClick={(e: MouseEvent<HTMLElement>) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:facebook' />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
href='/'
|
||||||
|
component={Link}
|
||||||
|
sx={{ color: '#1da1f2' }}
|
||||||
|
onClick={(e: MouseEvent<HTMLElement>) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:twitter' />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
href='/'
|
||||||
|
component={Link}
|
||||||
|
onClick={(e: MouseEvent<HTMLElement>) => e.preventDefault()}
|
||||||
|
sx={{ color: theme => (theme.palette.mode === 'light' ? '#272727' : 'grey.300') }}
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:github' />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
href='/'
|
||||||
|
component={Link}
|
||||||
|
sx={{ color: '#db4437' }}
|
||||||
|
onClick={(e: MouseEvent<HTMLElement>) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:google' />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</form>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</RightWrapper>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginPage.getLayout = (page: ReactNode) => <BlankLayout>{page}</BlankLayout>
|
||||||
|
|
||||||
|
LoginPage.guestGuard = true
|
||||||
|
|
||||||
|
export default LoginPage
|
||||||
12
app/login/page.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import LoginPage from './components/login'
|
||||||
|
|
||||||
|
function Login() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<LoginPage/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login
|
||||||
399
app/register/page.tsx
Normal file
@ -0,0 +1,399 @@
|
|||||||
|
"use client"
|
||||||
|
// ** React Imports
|
||||||
|
import { ReactNode, useState, Fragment, MouseEvent } from 'react'
|
||||||
|
|
||||||
|
// ** Next Import
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
// ** MUI Components
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import Divider from '@mui/material/Divider'
|
||||||
|
import Checkbox from '@mui/material/Checkbox'
|
||||||
|
import TextField from '@mui/material/TextField'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import InputLabel from '@mui/material/InputLabel'
|
||||||
|
import IconButton from '@mui/material/IconButton'
|
||||||
|
import Box, { BoxProps } from '@mui/material/Box'
|
||||||
|
import FormControl from '@mui/material/FormControl'
|
||||||
|
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||||
|
import OutlinedInput from '@mui/material/OutlinedInput'
|
||||||
|
import { styled, useTheme } from '@mui/material/styles'
|
||||||
|
import FormHelperText from '@mui/material/FormHelperText'
|
||||||
|
import InputAdornment from '@mui/material/InputAdornment'
|
||||||
|
import MuiFormControlLabel, { FormControlLabelProps } from '@mui/material/FormControlLabel'
|
||||||
|
|
||||||
|
// ** Icon Imports
|
||||||
|
import Icon from '../../src/@core/components/icon'
|
||||||
|
|
||||||
|
// ** Third Party Imports
|
||||||
|
import * as yup from 'yup'
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup'
|
||||||
|
import { useForm, Controller } from 'react-hook-form'
|
||||||
|
|
||||||
|
// ** Layout Import
|
||||||
|
import BlankLayout from '../../src/@core/layouts/BlankLayout'
|
||||||
|
|
||||||
|
// ** Hooks
|
||||||
|
import { useAuth } from '../../src/hooks/useAuth'
|
||||||
|
import { useSettings } from '../../src/@core/hooks/useSettings'
|
||||||
|
|
||||||
|
// ** Demo Imports
|
||||||
|
import FooterIllustrationsV2 from '../../src/views/pages/auth/FooterIllustrationsV2'
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
email: '',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
terms: false
|
||||||
|
}
|
||||||
|
interface FormData {
|
||||||
|
email: string
|
||||||
|
terms: boolean
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ** Styled Components
|
||||||
|
const RegisterIllustration = styled('img')(({ theme }) => ({
|
||||||
|
zIndex: 2,
|
||||||
|
maxHeight: 600,
|
||||||
|
marginTop: theme.spacing(12),
|
||||||
|
marginBottom: theme.spacing(12),
|
||||||
|
[theme.breakpoints.down(1540)]: {
|
||||||
|
maxHeight: 550
|
||||||
|
},
|
||||||
|
[theme.breakpoints.down('lg')]: {
|
||||||
|
maxHeight: 500
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const RightWrapper = styled(Box)<BoxProps>(({ theme }) => ({
|
||||||
|
width: '100%',
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
maxWidth: 450
|
||||||
|
},
|
||||||
|
[theme.breakpoints.up('lg')]: {
|
||||||
|
maxWidth: 600
|
||||||
|
},
|
||||||
|
[theme.breakpoints.up('xl')]: {
|
||||||
|
maxWidth: 750
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const LinkStyled = styled(Link)(({ theme }) => ({
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: theme.palette.primary.main
|
||||||
|
}))
|
||||||
|
|
||||||
|
const FormControlLabel = styled(MuiFormControlLabel)<FormControlLabelProps>(({ theme }) => ({
|
||||||
|
marginTop: theme.spacing(1.5),
|
||||||
|
marginBottom: theme.spacing(1.75),
|
||||||
|
'& .MuiFormControlLabel-label': {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
color: theme.palette.text.secondary
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const Register = () => {
|
||||||
|
// ** States
|
||||||
|
const [showPassword, setShowPassword] = useState<boolean>(false)
|
||||||
|
|
||||||
|
// ** Hooks
|
||||||
|
const theme = useTheme()
|
||||||
|
const { register } = useAuth()
|
||||||
|
const { settings } = useSettings()
|
||||||
|
const hidden = useMediaQuery(theme.breakpoints.down('md'))
|
||||||
|
|
||||||
|
// ** Vars
|
||||||
|
const { skin } = settings
|
||||||
|
const schema = yup.object().shape({
|
||||||
|
password: yup.string().min(5).required(),
|
||||||
|
username: yup.string().min(3).required(),
|
||||||
|
email: yup.string().email().required(),
|
||||||
|
terms: yup.bool().oneOf([true], 'You must accept the privacy policy & terms')
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
setError,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors }
|
||||||
|
} = useForm({
|
||||||
|
defaultValues,
|
||||||
|
mode: 'onBlur',
|
||||||
|
resolver: yupResolver(schema)
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = (data: FormData) => {
|
||||||
|
const { email, username, password } = data
|
||||||
|
register({ email, username, password }, err => {
|
||||||
|
if (err.email) {
|
||||||
|
setError('email', {
|
||||||
|
type: 'manual',
|
||||||
|
message: err.email
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (err.username) {
|
||||||
|
setError('username', {
|
||||||
|
type: 'manual',
|
||||||
|
message: err.username
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageSource = skin === 'bordered' ? 'auth-v2-register-illustration-bordered' : 'auth-v2-register-illustration'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className='content-right' sx={{ backgroundColor: 'background.paper' }}>
|
||||||
|
{!hidden ? (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
position: 'relative',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: '20px',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: 'customColors.bodyBg',
|
||||||
|
margin: theme => theme.spacing(8, 0, 8, 8)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RegisterIllustration
|
||||||
|
alt='register-illustration'
|
||||||
|
src={`/images/pages/${imageSource}-${theme.palette.mode}.png`}
|
||||||
|
/>
|
||||||
|
<FooterIllustrationsV2 />
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
<RightWrapper>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: [6, 12],
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ width: '100%', maxWidth: 400 }}>
|
||||||
|
<svg width={34} height={23.375} viewBox='0 0 32 22' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||||
|
<path
|
||||||
|
fillRule='evenodd'
|
||||||
|
clipRule='evenodd'
|
||||||
|
fill={theme.palette.primary.main}
|
||||||
|
d='M0.00172773 0V6.85398C0.00172773 6.85398 -0.133178 9.01207 1.98092 10.8388L13.6912 21.9964L19.7809 21.9181L18.8042 9.88248L16.4951 7.17289L9.23799 0H0.00172773Z'
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill='#161616'
|
||||||
|
opacity={0.06}
|
||||||
|
fillRule='evenodd'
|
||||||
|
clipRule='evenodd'
|
||||||
|
d='M7.69824 16.4364L12.5199 3.23696L16.5541 7.25596L7.69824 16.4364Z'
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill='#161616'
|
||||||
|
opacity={0.06}
|
||||||
|
fillRule='evenodd'
|
||||||
|
clipRule='evenodd'
|
||||||
|
d='M8.07751 15.9175L13.9419 4.63989L16.5849 7.28475L8.07751 15.9175Z'
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule='evenodd'
|
||||||
|
clipRule='evenodd'
|
||||||
|
fill={theme.palette.primary.main}
|
||||||
|
d='M7.77295 16.3566L23.6563 0H32V6.88383C32 6.88383 31.8262 9.17836 30.6591 10.4057L19.7824 22H13.6938L7.77295 16.3566Z'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<Box sx={{ my: 6 }}>
|
||||||
|
<Typography sx={{ mb: 1.5, fontWeight: 500, fontSize: '1.625rem', lineHeight: 1.385 }}>
|
||||||
|
Adventure starts here 🚀
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: 'text.secondary' }}>Make your app management easy and fun!</Typography>
|
||||||
|
</Box>
|
||||||
|
<form noValidate autoComplete='off' onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<FormControl fullWidth sx={{ mb: 4 }}>
|
||||||
|
<Controller
|
||||||
|
name='username'
|
||||||
|
control={control}
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field: { value, onChange, onBlur } }) => (
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
value={value}
|
||||||
|
onBlur={onBlur}
|
||||||
|
label='Username'
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder='johndoe'
|
||||||
|
error={Boolean(errors.username)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.username && (
|
||||||
|
<FormHelperText sx={{ color: 'error.main' }}>{errors.username.message}</FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
<FormControl fullWidth sx={{ mb: 4 }}>
|
||||||
|
<Controller
|
||||||
|
name='email'
|
||||||
|
control={control}
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field: { value, onChange, onBlur } }) => (
|
||||||
|
<TextField
|
||||||
|
value={value}
|
||||||
|
label='Email'
|
||||||
|
onBlur={onBlur}
|
||||||
|
onChange={onChange}
|
||||||
|
error={Boolean(errors.email)}
|
||||||
|
placeholder='user@email.com'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.email && <FormHelperText sx={{ color: 'error.main' }}>{errors.email.message}</FormHelperText>}
|
||||||
|
</FormControl>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel htmlFor='auth-login-v2-password' error={Boolean(errors.password)}>
|
||||||
|
Password
|
||||||
|
</InputLabel>
|
||||||
|
<Controller
|
||||||
|
name='password'
|
||||||
|
control={control}
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field: { value, onChange, onBlur } }) => (
|
||||||
|
<OutlinedInput
|
||||||
|
value={value}
|
||||||
|
label='Password'
|
||||||
|
onBlur={onBlur}
|
||||||
|
onChange={onChange}
|
||||||
|
id='auth-login-v2-password'
|
||||||
|
error={Boolean(errors.password)}
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
endAdornment={
|
||||||
|
<InputAdornment position='end'>
|
||||||
|
<IconButton
|
||||||
|
edge='end'
|
||||||
|
onMouseDown={e => e.preventDefault()}
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
>
|
||||||
|
<Icon icon={showPassword ? 'tabler:eye' : 'tabler:eye-off'} fontSize={20} />
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.password && (
|
||||||
|
<FormHelperText sx={{ color: 'error.main' }}>{errors.password.message}</FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl error={Boolean(errors.terms)}>
|
||||||
|
<Controller
|
||||||
|
name='terms'
|
||||||
|
control={control}
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field: { value, onChange } }) => {
|
||||||
|
return (
|
||||||
|
<FormControlLabel
|
||||||
|
sx={{
|
||||||
|
...(errors.terms ? { color: 'error.main' } : null),
|
||||||
|
'& .MuiFormControlLabel-label': { fontSize: '0.875rem' }
|
||||||
|
}}
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={value}
|
||||||
|
onChange={onChange}
|
||||||
|
sx={errors.terms ? { color: 'error.main' } : null}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<Fragment>
|
||||||
|
<Typography
|
||||||
|
variant='body2'
|
||||||
|
component='span'
|
||||||
|
sx={{ color: errors.terms ? 'error.main' : '' }}
|
||||||
|
>
|
||||||
|
I agree to{' '}
|
||||||
|
</Typography>
|
||||||
|
<LinkStyled href='/' onClick={(e: MouseEvent<HTMLElement>) => e.preventDefault()}>
|
||||||
|
privacy policy & terms
|
||||||
|
</LinkStyled>
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.terms && (
|
||||||
|
<FormHelperText sx={{ mt: 0, color: 'error.main' }}>{errors.terms.message}</FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
<Button fullWidth size='large' type='submit' variant='contained' sx={{ mb: 4 }}>
|
||||||
|
Sign up
|
||||||
|
</Button>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', justifyContent: 'center' }}>
|
||||||
|
<Typography sx={{ color: 'text.secondary', mr: 2 }}>Already have an account?</Typography>
|
||||||
|
<Typography variant='body2'>
|
||||||
|
<LinkStyled href='/login' sx={{ fontSize: '1rem' }}>
|
||||||
|
Sign in instead
|
||||||
|
</LinkStyled>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider
|
||||||
|
sx={{
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
color: 'text.disabled',
|
||||||
|
'& .MuiDivider-wrapper': { px: 6 },
|
||||||
|
my: theme => `${theme.spacing(6)} !important`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
or
|
||||||
|
</Divider>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<IconButton
|
||||||
|
href='/'
|
||||||
|
component={Link}
|
||||||
|
sx={{ color: '#497ce2' }}
|
||||||
|
onClick={(e: MouseEvent<HTMLElement>) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:facebook' />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
href='/'
|
||||||
|
component={Link}
|
||||||
|
sx={{ color: '#1da1f2' }}
|
||||||
|
onClick={(e: MouseEvent<HTMLElement>) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:twitter' />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
href='/'
|
||||||
|
component={Link}
|
||||||
|
onClick={(e: MouseEvent<HTMLElement>) => e.preventDefault()}
|
||||||
|
sx={{ color: theme => (theme.palette.mode === 'light' ? '#272727' : 'grey.300') }}
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:github' />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
href='/'
|
||||||
|
component={Link}
|
||||||
|
sx={{ color: '#db4437' }}
|
||||||
|
onClick={(e: MouseEvent<HTMLElement>) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:google' />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</form>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</RightWrapper>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Register.getLayout = (page: ReactNode) => <BlankLayout>{page}</BlankLayout>
|
||||||
|
|
||||||
|
Register.guestGuard = true
|
||||||
|
|
||||||
|
export default Register
|
||||||
11
next.config.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
async redirects() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/',
|
||||||
|
destination: '/login',
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -1,4 +0,0 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
|
||||||
const nextConfig = {};
|
|
||||||
|
|
||||||
export default nextConfig;
|
|
||||||
113
package.json
@ -9,18 +9,109 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18",
|
"@casl/ability": "6.3.3",
|
||||||
"react-dom": "^18",
|
"@casl/react": "3.1.0",
|
||||||
"next": "14.2.1"
|
"@emotion/cache": "11.10.5",
|
||||||
|
"@emotion/react": "11.10.5",
|
||||||
|
"@emotion/server": "11.10.0",
|
||||||
|
"@emotion/styled": "11.10.5",
|
||||||
|
"@fullcalendar/bootstrap5": "6.0.2",
|
||||||
|
"@fullcalendar/common": "5.11.3",
|
||||||
|
"@fullcalendar/core": "6.0.2",
|
||||||
|
"@fullcalendar/daygrid": "6.0.2",
|
||||||
|
"@fullcalendar/interaction": "6.0.2",
|
||||||
|
"@fullcalendar/list": "6.0.2",
|
||||||
|
"@fullcalendar/react": "6.0.2",
|
||||||
|
"@fullcalendar/timegrid": "6.0.2",
|
||||||
|
"@hookform/resolvers": "^3.3.4",
|
||||||
|
"@iconify/react": "4.0.1",
|
||||||
|
"@mui/lab": "5.0.0-alpha.115",
|
||||||
|
"@mui/material": "5.11.4",
|
||||||
|
"@mui/x-data-grid": "5.17.18",
|
||||||
|
"@popperjs/core": "2.11.6",
|
||||||
|
"@reduxjs/toolkit": "1.9.1",
|
||||||
|
"apexcharts-clevision": "3.28.5",
|
||||||
|
"axios": "1.2.2",
|
||||||
|
"axios-mock-adapter": "1.21.2",
|
||||||
|
"bootstrap-icons": "1.10.3",
|
||||||
|
"chart.js": "4.1.2",
|
||||||
|
"cleave.js": "1.6.0",
|
||||||
|
"clipboard-copy": "4.0.1",
|
||||||
|
"clsx": "1.2.1",
|
||||||
|
"date-fns": "2.29.3",
|
||||||
|
"draft-js": "0.11.7",
|
||||||
|
"i18next": "22.4.9",
|
||||||
|
"i18next-browser-languagedetector": "7.0.1",
|
||||||
|
"i18next-http-backend": "2.1.1",
|
||||||
|
"jsonwebtoken": "8.5.1",
|
||||||
|
"keen-slider": "6.8.5",
|
||||||
|
"next": "^14.2.1",
|
||||||
|
"nprogress": "0.2.0",
|
||||||
|
"payment": "2.4.6",
|
||||||
|
"prismjs": "1.29.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-apexcharts": "1.4.0",
|
||||||
|
"react-chartjs-2": "5.1.0",
|
||||||
|
"react-credit-cards": "0.8.3",
|
||||||
|
"react-datepicker": "4.8.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-draft-wysiwyg": "1.15.0",
|
||||||
|
"react-dropzone": "14.2.3",
|
||||||
|
"react-hook-form": "7.41.5",
|
||||||
|
"react-hot-toast": "2.4.0",
|
||||||
|
"react-i18next": "12.1.4",
|
||||||
|
"react-perfect-scrollbar": "1.5.8",
|
||||||
|
"react-popper": "2.3.0",
|
||||||
|
"react-redux": "8.0.5",
|
||||||
|
"recharts": "2.2.0",
|
||||||
|
"stylis": "4.1.3",
|
||||||
|
"stylis-plugin-rtl": "2.1.1",
|
||||||
|
"tailwindcss": "^3.4.3",
|
||||||
|
"yup": "0.32.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
"@iconify/iconify": "3.0.1",
|
||||||
"@types/node": "^20",
|
"@iconify/json": "2.2.4",
|
||||||
"@types/react": "^18",
|
"@iconify/tools": "2.2.0",
|
||||||
"@types/react-dom": "^18",
|
"@iconify/types": "2.0.0",
|
||||||
"postcss": "^8",
|
"@iconify/utils": "2.0.11",
|
||||||
"tailwindcss": "^3.4.1",
|
"@types/cleave.js": "1.4.7",
|
||||||
"eslint": "^8",
|
"@types/draft-js": "0.11.10",
|
||||||
"eslint-config-next": "14.2.1"
|
"@types/jsonwebtoken": "8.5.9",
|
||||||
|
"@types/node": "18.11.18",
|
||||||
|
"@types/nprogress": "0.2.0",
|
||||||
|
"@types/payment": "2.1.4",
|
||||||
|
"@types/prismjs": "1.26.0",
|
||||||
|
"@types/react": "^18.2.79",
|
||||||
|
"@types/react-credit-cards": "0.8.1",
|
||||||
|
"@types/react-datepicker": "4.8.0",
|
||||||
|
"@types/react-draft-wysiwyg": "1.13.4",
|
||||||
|
"@types/react-redux": "7.1.25",
|
||||||
|
"@typescript-eslint/eslint-plugin": "5.48.0",
|
||||||
|
"@typescript-eslint/parser": "5.48.0",
|
||||||
|
"eslint": "8.31.0",
|
||||||
|
"eslint-config-next": "^14.2.1",
|
||||||
|
"eslint-config-prettier": "8.6.0",
|
||||||
|
"eslint-import-resolver-alias": "1.1.2",
|
||||||
|
"eslint-import-resolver-typescript": "3.5.2",
|
||||||
|
"eslint-plugin-import": "2.26.0",
|
||||||
|
"prettier": "2.8.2",
|
||||||
|
"typescript": "4.9.4"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"minipass": "4.0.0",
|
||||||
|
"@mui/x-data-grid/@mui/system": "5.4.1",
|
||||||
|
"react-credit-cards/prop-types": "15.7.2",
|
||||||
|
"react-hot-toast/goober/csstype": "3.0.10",
|
||||||
|
"recharts/react-smooth/prop-types": "15.6.0",
|
||||||
|
"react-draft-wysiwyg/html-to-draftjs/immutable": "4.2.2",
|
||||||
|
"react-draft-wysiwyg/draftjs-utils/immutable": "4.2.2",
|
||||||
|
"@emotion/react/@emotion/babel-plugin/@babel/core": "7.0.0",
|
||||||
|
"@emotion/react/@emotion/babel-plugin/@babel/plugin-syntax-jsx/@babel/core": "7.0.0"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"react-credit-cards": {
|
||||||
|
"react": "$react"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/images/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
public/images/avatars/1.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
public/images/favicon.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/images/pages/401.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
public/images/pages/404.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 75 KiB |
BIN
public/images/pages/auth-v2-login-illustration-bordered-dark.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 47 KiB |
BIN
public/images/pages/auth-v2-login-illustration-dark.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
public/images/pages/auth-v2-login-illustration-light.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
public/images/pages/auth-v2-mask-dark.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/images/pages/auth-v2-mask-light.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 75 KiB |
BIN
public/images/pages/auth-v2-register-illustration-dark.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
public/images/pages/auth-v2-register-illustration-light.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
public/images/pages/misc-mask-dark.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
public/images/pages/misc-mask-light.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
@ -1 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
|
||||||
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 629 B After Width: | Height: | Size: 1.1 KiB |
62
src/@core/components/auth/AclGuard.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { ReactNode, useState } from 'react'
|
||||||
|
|
||||||
|
// ** Next Import
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import type { ACLObj, AppAbility } from 'src/configs/acl'
|
||||||
|
|
||||||
|
// ** Context Imports
|
||||||
|
import { AbilityContext } from 'src/layouts/components/acl/Can'
|
||||||
|
|
||||||
|
// ** Config Import
|
||||||
|
import { buildAbilityFor } from 'src/configs/acl'
|
||||||
|
|
||||||
|
// ** Component Import
|
||||||
|
import NotAuthorized from 'src/pages/401'
|
||||||
|
import BlankLayout from 'src/@core/layouts/BlankLayout'
|
||||||
|
|
||||||
|
// ** Hooks
|
||||||
|
import { useAuth } from 'src/hooks/useAuth'
|
||||||
|
|
||||||
|
interface AclGuardProps {
|
||||||
|
children: ReactNode
|
||||||
|
guestGuard: boolean
|
||||||
|
aclAbilities: ACLObj
|
||||||
|
}
|
||||||
|
|
||||||
|
const AclGuard = (props: AclGuardProps) => {
|
||||||
|
// ** Props
|
||||||
|
const { aclAbilities, children, guestGuard } = props
|
||||||
|
|
||||||
|
const [ability, setAbility] = useState<AppAbility | undefined>(undefined)
|
||||||
|
|
||||||
|
// ** Hooks
|
||||||
|
const auth = useAuth()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// If guestGuard is true and user is not logged in or its an error page, render the page without checking access
|
||||||
|
if (guestGuard || router.route === '/404' || router.route === '/500' || router.route === '/') {
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
// User is logged in, build ability for the user based on his role
|
||||||
|
if (auth.user && auth.user.role && !ability) {
|
||||||
|
setAbility(buildAbilityFor(auth.user.role, aclAbilities.subject))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the access of current user and render pages
|
||||||
|
if (ability && ability.can(aclAbilities.action, aclAbilities.subject)) {
|
||||||
|
return <AbilityContext.Provider value={ability}>{children}</AbilityContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Not Authorized component if the current user has limited access
|
||||||
|
return (
|
||||||
|
<BlankLayout>
|
||||||
|
<NotAuthorized />
|
||||||
|
</BlankLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AclGuard
|
||||||
48
src/@core/components/auth/AuthGuard.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { ReactNode, ReactElement, useEffect } from 'react'
|
||||||
|
|
||||||
|
// ** Next Import
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
// ** Hooks Import
|
||||||
|
import { useAuth } from 'src/hooks/useAuth'
|
||||||
|
|
||||||
|
interface AuthGuardProps {
|
||||||
|
children: ReactNode
|
||||||
|
fallback: ReactElement | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthGuard = (props: AuthGuardProps) => {
|
||||||
|
const { children, fallback } = props
|
||||||
|
const auth = useAuth()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
if (!router.isReady) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth.user === null && !window.localStorage.getItem('userData')) {
|
||||||
|
if (router.asPath !== '/') {
|
||||||
|
router.replace({
|
||||||
|
pathname: '/login',
|
||||||
|
query: { returnUrl: router.asPath }
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
router.replace('/login')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[router.route]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (auth.loading || auth.user === null) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthGuard
|
||||||
38
src/@core/components/auth/GuestGuard.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { ReactNode, ReactElement, useEffect } from 'react'
|
||||||
|
|
||||||
|
// ** Next Import
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
// ** Hooks Import
|
||||||
|
import { useAuth } from 'src/hooks/useAuth'
|
||||||
|
|
||||||
|
interface GuestGuardProps {
|
||||||
|
children: ReactNode
|
||||||
|
fallback: ReactElement | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const GuestGuard = (props: GuestGuardProps) => {
|
||||||
|
const { children, fallback } = props
|
||||||
|
const auth = useAuth()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!router.isReady) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.localStorage.getItem('userData')) {
|
||||||
|
router.replace('/')
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [router.route])
|
||||||
|
|
||||||
|
if (auth.loading || (!auth.loading && auth.user !== null)) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GuestGuard
|
||||||
138
src/@core/components/card-snippet/index.tsx
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
// ** MUI Imports
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Card from '@mui/material/Card'
|
||||||
|
import Tooltip from '@mui/material/Tooltip'
|
||||||
|
import Divider from '@mui/material/Divider'
|
||||||
|
import { Theme } from '@mui/material/styles'
|
||||||
|
import Collapse from '@mui/material/Collapse'
|
||||||
|
import IconButton from '@mui/material/IconButton'
|
||||||
|
import CardHeader from '@mui/material/CardHeader'
|
||||||
|
import CardContent from '@mui/material/CardContent'
|
||||||
|
import ToggleButton from '@mui/material/ToggleButton'
|
||||||
|
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||||
|
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'
|
||||||
|
|
||||||
|
// ** Icon Imports
|
||||||
|
import Icon from 'src/@core/components/icon'
|
||||||
|
|
||||||
|
// ** Third Party Components
|
||||||
|
import Prism from 'prismjs'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { CardSnippetProps } from './types'
|
||||||
|
|
||||||
|
// ** Hooks
|
||||||
|
import useClipboard from 'src/@core/hooks/useClipboard'
|
||||||
|
|
||||||
|
const CardSnippet = (props: CardSnippetProps) => {
|
||||||
|
// ** Props
|
||||||
|
const { id, sx, code, title, children, className } = props
|
||||||
|
|
||||||
|
// ** States
|
||||||
|
const [showCode, setShowCode] = useState<boolean>(false)
|
||||||
|
const [tabValue, setTabValue] = useState<'tsx' | 'jsx'>(code.tsx !== null ? 'tsx' : 'jsx')
|
||||||
|
|
||||||
|
// ** Hooks
|
||||||
|
const clipboard = useClipboard()
|
||||||
|
const hidden = useMediaQuery((theme: Theme) => theme.breakpoints.down('md'))
|
||||||
|
|
||||||
|
// ** Highlight code on mount
|
||||||
|
useEffect(() => {
|
||||||
|
Prism.highlightAll()
|
||||||
|
}, [showCode, tabValue])
|
||||||
|
|
||||||
|
const codeToCopy = () => {
|
||||||
|
if (code.tsx !== null && tabValue === 'tsx') {
|
||||||
|
return code.tsx.props.children.props.children
|
||||||
|
} else if (code.jsx !== null && tabValue === 'jsx') {
|
||||||
|
return code.jsx.props.children.props.children
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
clipboard.copy(codeToCopy())
|
||||||
|
toast.success('The source code has been copied to your clipboard.', {
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderCode = () => {
|
||||||
|
if (code[tabValue] !== null) {
|
||||||
|
return code[tabValue]
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={className}
|
||||||
|
sx={{ '& .MuiCardHeader-action': { lineHeight: 0.8 }, ...sx }}
|
||||||
|
id={id || `card-snippet--${title.toLowerCase().replace(/ /g, '-')}`}
|
||||||
|
>
|
||||||
|
<CardHeader
|
||||||
|
title={title}
|
||||||
|
{...(hidden
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
action: (
|
||||||
|
<IconButton onClick={() => setShowCode(!showCode)}>
|
||||||
|
<Icon icon='tabler:code' fontSize={20} />
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<CardContent>{children}</CardContent>
|
||||||
|
{hidden ? null : (
|
||||||
|
<Collapse in={showCode}>
|
||||||
|
<Divider sx={{ my: '0 !important' }} />
|
||||||
|
|
||||||
|
<CardContent sx={{ position: 'relative', '& pre': { m: '0 !important', maxHeight: 500 } }}>
|
||||||
|
<Box sx={{ mb: 4, display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
exclusive
|
||||||
|
size='small'
|
||||||
|
color='primary'
|
||||||
|
value={tabValue}
|
||||||
|
onChange={(e, newValue) => (newValue !== null ? setTabValue(newValue) : null)}
|
||||||
|
>
|
||||||
|
{code.tsx !== null ? (
|
||||||
|
<ToggleButton value='tsx'>
|
||||||
|
<Icon icon='tabler:brand-typescript' fontSize={20} />
|
||||||
|
</ToggleButton>
|
||||||
|
) : null}
|
||||||
|
{code.jsx !== null ? (
|
||||||
|
<ToggleButton value='jsx'>
|
||||||
|
<Icon icon='tabler:brand-javascript' fontSize={20} />
|
||||||
|
</ToggleButton>
|
||||||
|
) : null}
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</Box>
|
||||||
|
<Tooltip title='Copy the source' placement='top'>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleClick}
|
||||||
|
sx={{
|
||||||
|
top: '5rem',
|
||||||
|
color: 'grey.100',
|
||||||
|
right: '2.5625rem',
|
||||||
|
position: 'absolute'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon='tabler:copy' fontSize={20} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<div>{renderCode()}</div>
|
||||||
|
</CardContent>
|
||||||
|
</Collapse>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CardSnippet
|
||||||
16
src/@core/components/card-snippet/types.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { ReactNode, ReactElement } from 'react'
|
||||||
|
|
||||||
|
// ** ateMUI Imports
|
||||||
|
import { CardProps } from '@mui/material/Card'
|
||||||
|
|
||||||
|
export type CardSnippetProps = CardProps & {
|
||||||
|
id?: string
|
||||||
|
title: string
|
||||||
|
children: ReactNode
|
||||||
|
code: {
|
||||||
|
tsx: ReactElement | null
|
||||||
|
jsx: ReactElement | null
|
||||||
|
}
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Card from '@mui/material/Card'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import CardContent from '@mui/material/CardContent'
|
||||||
|
|
||||||
|
// ** Type Import
|
||||||
|
import { CardStatsHorizontalWithDetailsProps } from 'src/@core/components/card-statistics/types'
|
||||||
|
|
||||||
|
// ** Custom Component Import
|
||||||
|
import Icon from 'src/@core/components/icon'
|
||||||
|
import CustomAvatar from 'src/@core/components/mui/avatar'
|
||||||
|
|
||||||
|
const CardStatsHorizontalWithDetails = (props: CardStatsHorizontalWithDetailsProps) => {
|
||||||
|
// ** Props
|
||||||
|
const {
|
||||||
|
sx,
|
||||||
|
icon,
|
||||||
|
stats,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
trendDiff,
|
||||||
|
iconSize = 24,
|
||||||
|
avatarSize = 38,
|
||||||
|
trend = 'positive',
|
||||||
|
avatarColor = 'primary'
|
||||||
|
} = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card sx={{ ...sx }}>
|
||||||
|
<CardContent sx={{ gap: 3, display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||||
|
<Typography sx={{ mb: 1, color: 'text.secondary' }}>{title}</Typography>
|
||||||
|
<Box sx={{ mb: 1, columnGap: 1.5, display: 'flex', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
|
<Typography variant='h5'>{stats}</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={{ color: trend === 'negative' ? 'error.main' : 'success.main' }}
|
||||||
|
>{`(${trendDiff})%`}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography sx={{ color: 'text.secondary' }}>{subtitle}</Typography>
|
||||||
|
</Box>
|
||||||
|
<CustomAvatar skin='light' variant='rounded' color={avatarColor} sx={{ width: avatarSize, height: avatarSize }}>
|
||||||
|
<Icon icon={icon} fontSize={iconSize} />
|
||||||
|
</CustomAvatar>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CardStatsHorizontalWithDetails
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Card from '@mui/material/Card'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import CardContent from '@mui/material/CardContent'
|
||||||
|
|
||||||
|
// ** Type Import
|
||||||
|
import { CardStatsHorizontalProps } from 'src/@core/components/card-statistics/types'
|
||||||
|
|
||||||
|
// ** Custom Component Import
|
||||||
|
import Icon from 'src/@core/components/icon'
|
||||||
|
import CustomAvatar from 'src/@core/components/mui/avatar'
|
||||||
|
|
||||||
|
const CardStatsHorizontal = (props: CardStatsHorizontalProps) => {
|
||||||
|
// ** Props
|
||||||
|
const { sx, icon, stats, title, iconSize = 24, avatarSize = 42, avatarColor = 'primary' } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card sx={{ ...sx }}>
|
||||||
|
<CardContent sx={{ gap: 3, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||||
|
<Typography variant='h6'>{stats}</Typography>
|
||||||
|
<Typography variant='body2'>{title}</Typography>
|
||||||
|
</Box>
|
||||||
|
<CustomAvatar skin='light' color={avatarColor} sx={{ width: avatarSize, height: avatarSize }}>
|
||||||
|
<Icon icon={icon} fontSize={iconSize} />
|
||||||
|
</CustomAvatar>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CardStatsHorizontal
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import Card from '@mui/material/Card'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import CardContent from '@mui/material/CardContent'
|
||||||
|
|
||||||
|
// ** Type Import
|
||||||
|
import { CardStatsSquareProps } from 'src/@core/components/card-statistics/types'
|
||||||
|
|
||||||
|
// ** Custom Component Import
|
||||||
|
import Icon from 'src/@core/components/icon'
|
||||||
|
import CustomAvatar from 'src/@core/components/mui/avatar'
|
||||||
|
|
||||||
|
const CardStatsSquare = (props: CardStatsSquareProps) => {
|
||||||
|
// ** Props
|
||||||
|
const { sx, icon, stats, title, iconSize = 24, avatarSize = 42, avatarColor = 'primary' } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card sx={{ ...sx }}>
|
||||||
|
<CardContent sx={{ display: 'flex', alignItems: 'center', flexDirection: 'column' }}>
|
||||||
|
<CustomAvatar skin='light' color={avatarColor} sx={{ mb: 2, width: avatarSize, height: avatarSize }}>
|
||||||
|
<Icon icon={icon} fontSize={iconSize} />
|
||||||
|
</CustomAvatar>
|
||||||
|
<Typography variant='h6' sx={{ mb: 2 }}>
|
||||||
|
{stats}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body2'>{title}</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CardStatsSquare
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import Card from '@mui/material/Card'
|
||||||
|
import Chip from '@mui/material/Chip'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import CardContent from '@mui/material/CardContent'
|
||||||
|
|
||||||
|
// ** Type Import
|
||||||
|
import { CardStatsVerticalProps } from 'src/@core/components/card-statistics/types'
|
||||||
|
|
||||||
|
// ** Custom Component Import
|
||||||
|
import Icon from 'src/@core/components/icon'
|
||||||
|
import CustomChip from 'src/@core/components/mui/chip'
|
||||||
|
import CustomAvatar from 'src/@core/components/mui/avatar'
|
||||||
|
|
||||||
|
const CardStatsVertical = (props: CardStatsVerticalProps) => {
|
||||||
|
// ** Props
|
||||||
|
const {
|
||||||
|
sx,
|
||||||
|
stats,
|
||||||
|
title,
|
||||||
|
chipText,
|
||||||
|
subtitle,
|
||||||
|
avatarIcon,
|
||||||
|
iconSize = 24,
|
||||||
|
avatarSize = 42,
|
||||||
|
chipColor = 'primary',
|
||||||
|
avatarColor = 'primary'
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const RenderChip = chipColor === 'default' ? Chip : CustomChip
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card sx={{ ...sx }}>
|
||||||
|
<CardContent sx={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||||
|
<CustomAvatar
|
||||||
|
skin='light'
|
||||||
|
variant='rounded'
|
||||||
|
color={avatarColor}
|
||||||
|
sx={{ mb: 3.5, width: avatarSize, height: avatarSize }}
|
||||||
|
>
|
||||||
|
<Icon icon={avatarIcon} fontSize={iconSize} />
|
||||||
|
</CustomAvatar>
|
||||||
|
<Typography variant='h6' sx={{ mb: 1 }}>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body2' sx={{ mb: 1, color: 'text.disabled' }}>
|
||||||
|
{subtitle}
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ mb: 3.5, color: 'text.secondary' }}>{stats}</Typography>
|
||||||
|
<RenderChip
|
||||||
|
size='small'
|
||||||
|
label={chipText}
|
||||||
|
color={chipColor}
|
||||||
|
{...(chipColor === 'default'
|
||||||
|
? { sx: { borderRadius: '4px', color: 'text.secondary' } }
|
||||||
|
: { rounded: true, skin: 'light' })}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CardStatsVertical
|
||||||
@ -0,0 +1,105 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import Card from '@mui/material/Card'
|
||||||
|
import { useTheme } from '@mui/material/styles'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import CardContent from '@mui/material/CardContent'
|
||||||
|
|
||||||
|
// ** Type Imports
|
||||||
|
import { ApexOptions } from 'apexcharts'
|
||||||
|
import { CardStatsWithAreaChartProps } from 'src/@core/components/card-statistics/types'
|
||||||
|
|
||||||
|
// ** Custom Component Imports
|
||||||
|
import Icon from 'src/@core/components/icon'
|
||||||
|
import CustomAvatar from 'src/@core/components/mui/avatar'
|
||||||
|
import ReactApexcharts from 'src/@core/components/react-apexcharts'
|
||||||
|
|
||||||
|
const CardStatsWithAreaChart = (props: CardStatsWithAreaChartProps) => {
|
||||||
|
// ** Props
|
||||||
|
const {
|
||||||
|
sx,
|
||||||
|
stats,
|
||||||
|
title,
|
||||||
|
avatarIcon,
|
||||||
|
chartSeries,
|
||||||
|
avatarSize = 42,
|
||||||
|
avatarIconSize = 26,
|
||||||
|
chartColor = 'primary',
|
||||||
|
avatarColor = 'primary'
|
||||||
|
} = props
|
||||||
|
|
||||||
|
// ** Hook
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
const options: ApexOptions = {
|
||||||
|
chart: {
|
||||||
|
parentHeightOffset: 0,
|
||||||
|
toolbar: { show: false },
|
||||||
|
sparkline: { enabled: true }
|
||||||
|
},
|
||||||
|
tooltip: { enabled: false },
|
||||||
|
dataLabels: { enabled: false },
|
||||||
|
stroke: {
|
||||||
|
width: 2.5,
|
||||||
|
curve: 'smooth'
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
show: false,
|
||||||
|
padding: {
|
||||||
|
top: 2,
|
||||||
|
bottom: 17
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fill: {
|
||||||
|
type: 'gradient',
|
||||||
|
gradient: {
|
||||||
|
opacityTo: 0,
|
||||||
|
opacityFrom: 1,
|
||||||
|
shadeIntensity: 1,
|
||||||
|
stops: [0, 100],
|
||||||
|
colorStops: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
offset: 0,
|
||||||
|
opacity: 0.4,
|
||||||
|
color: theme.palette[chartColor].main
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 100,
|
||||||
|
opacity: 0.1,
|
||||||
|
color: theme.palette.background.paper
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
monochrome: {
|
||||||
|
enabled: true,
|
||||||
|
shadeTo: 'light',
|
||||||
|
shadeIntensity: 1,
|
||||||
|
color: theme.palette[chartColor].main
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
labels: { show: false },
|
||||||
|
axisTicks: { show: false },
|
||||||
|
axisBorder: { show: false }
|
||||||
|
},
|
||||||
|
yaxis: { show: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card sx={{ ...sx }}>
|
||||||
|
<CardContent sx={{ display: 'flex', pb: '0 !important', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||||
|
<CustomAvatar skin='light' color={avatarColor} sx={{ mb: 2.5, width: avatarSize, height: avatarSize }}>
|
||||||
|
<Icon icon={avatarIcon} fontSize={avatarIconSize} />
|
||||||
|
</CustomAvatar>
|
||||||
|
<Typography variant='h6'>{stats}</Typography>
|
||||||
|
<Typography variant='body2'>{title}</Typography>
|
||||||
|
</CardContent>
|
||||||
|
<ReactApexcharts type='area' height={106} options={options} series={chartSeries} />
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CardStatsWithAreaChart
|
||||||
63
src/@core/components/card-statistics/types.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// ** Types
|
||||||
|
import { ApexOptions } from 'apexcharts'
|
||||||
|
import { ChipProps } from '@mui/material/Chip'
|
||||||
|
import { SxProps, Theme } from '@mui/material'
|
||||||
|
import { ThemeColor } from 'src/@core/layouts/types'
|
||||||
|
|
||||||
|
export type CardStatsSquareProps = {
|
||||||
|
icon: string
|
||||||
|
stats: string
|
||||||
|
title: string
|
||||||
|
sx?: SxProps<Theme>
|
||||||
|
avatarSize?: number
|
||||||
|
avatarColor?: ThemeColor
|
||||||
|
iconSize?: number | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CardStatsHorizontalProps = {
|
||||||
|
icon: string
|
||||||
|
stats: string
|
||||||
|
title: string
|
||||||
|
sx?: SxProps<Theme>
|
||||||
|
avatarSize?: number
|
||||||
|
avatarColor?: ThemeColor
|
||||||
|
iconSize?: number | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CardStatsWithAreaChartProps = {
|
||||||
|
stats: string
|
||||||
|
title: string
|
||||||
|
avatarIcon: string
|
||||||
|
sx?: SxProps<Theme>
|
||||||
|
avatarSize?: number
|
||||||
|
chartColor?: ThemeColor
|
||||||
|
avatarColor?: ThemeColor
|
||||||
|
avatarIconSize?: number | string
|
||||||
|
chartSeries: ApexOptions['series']
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CardStatsVerticalProps = {
|
||||||
|
stats: string
|
||||||
|
title: string
|
||||||
|
chipText: string
|
||||||
|
subtitle: string
|
||||||
|
avatarIcon: string
|
||||||
|
sx?: SxProps<Theme>
|
||||||
|
avatarSize?: number
|
||||||
|
avatarColor?: ThemeColor
|
||||||
|
iconSize?: number | string
|
||||||
|
chipColor?: ChipProps['color']
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CardStatsHorizontalWithDetailsProps = {
|
||||||
|
icon: string
|
||||||
|
stats: string
|
||||||
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
trendDiff: string
|
||||||
|
sx?: SxProps<Theme>
|
||||||
|
avatarSize?: number
|
||||||
|
avatarColor?: ThemeColor
|
||||||
|
iconSize?: number | string
|
||||||
|
trend?: 'positive' | 'negative'
|
||||||
|
}
|
||||||
94
src/@core/components/custom-checkbox/basic/index.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Grid from '@mui/material/Grid'
|
||||||
|
import Checkbox from '@mui/material/Checkbox'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
|
||||||
|
// ** Type Imports
|
||||||
|
import { CustomCheckboxBasicProps } from 'src/@core/components/custom-checkbox/types'
|
||||||
|
|
||||||
|
const CustomCheckbox = (props: CustomCheckboxBasicProps) => {
|
||||||
|
// ** Props
|
||||||
|
const { data, name, selected, gridProps, handleChange, color = 'primary' } = props
|
||||||
|
|
||||||
|
const { meta, title, value, content } = data
|
||||||
|
|
||||||
|
const renderData = () => {
|
||||||
|
if (meta && title && content) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mb: 2,
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{typeof title === 'string' ? <Typography sx={{ mr: 2, fontWeight: 500 }}>{title}</Typography> : title}
|
||||||
|
{typeof meta === 'string' ? <Typography sx={{ color: 'text.disabled' }}>{meta}</Typography> : meta}
|
||||||
|
</Box>
|
||||||
|
{typeof content === 'string' ? <Typography variant='body2'>{content}</Typography> : content}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
} else if (meta && title && !content) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: '100%', display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' }}>
|
||||||
|
{typeof title === 'string' ? <Typography sx={{ mr: 2, fontWeight: 500 }}>{title}</Typography> : title}
|
||||||
|
{typeof meta === 'string' ? <Typography sx={{ color: 'text.disabled' }}>{meta}</Typography> : meta}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
} else if (!meta && title && content) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
{typeof title === 'string' ? <Typography sx={{ mb: 1, fontWeight: 500 }}>{title}</Typography> : title}
|
||||||
|
{typeof content === 'string' ? <Typography variant='body2'>{content}</Typography> : content}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
} else if (!meta && !title && content) {
|
||||||
|
return typeof content === 'string' ? <Typography variant='body2'>{content}</Typography> : content
|
||||||
|
} else if (!meta && title && !content) {
|
||||||
|
return typeof title === 'string' ? <Typography sx={{ fontWeight: 500 }}>{title}</Typography> : title
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderComponent = () => {
|
||||||
|
return (
|
||||||
|
<Grid item {...gridProps}>
|
||||||
|
<Box
|
||||||
|
onClick={() => handleChange(value)}
|
||||||
|
sx={{
|
||||||
|
p: 4,
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
borderRadius: 1,
|
||||||
|
cursor: 'pointer',
|
||||||
|
position: 'relative',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
border: theme => `1px solid ${theme.palette.divider}`,
|
||||||
|
...(selected.includes(value)
|
||||||
|
? { borderColor: `${color}.main` }
|
||||||
|
: { '&:hover': { borderColor: theme => `rgba(${theme.palette.customColors.main}, 0.25)` } })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
size='small'
|
||||||
|
color={color}
|
||||||
|
name={`${name}-${value}`}
|
||||||
|
checked={selected.includes(value)}
|
||||||
|
sx={{ mb: -2, mt: -2.5, ml: -2.75 }}
|
||||||
|
onChange={() => handleChange(value)}
|
||||||
|
/>
|
||||||
|
{renderData()}
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data ? renderComponent() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomCheckbox
|
||||||
72
src/@core/components/custom-checkbox/icons/index.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Grid from '@mui/material/Grid'
|
||||||
|
import Checkbox from '@mui/material/Checkbox'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
|
||||||
|
// ** Type Imports
|
||||||
|
import { CustomCheckboxIconsProps } from 'src/@core/components/custom-checkbox/types'
|
||||||
|
|
||||||
|
// ** Icon Imports
|
||||||
|
import Icon from 'src/@core/components/icon'
|
||||||
|
|
||||||
|
const CustomCheckboxIcons = (props: CustomCheckboxIconsProps) => {
|
||||||
|
// ** Props
|
||||||
|
const { data, icon, name, selected, gridProps, iconProps, handleChange, color = 'primary' } = props
|
||||||
|
|
||||||
|
const { title, value, content } = data
|
||||||
|
|
||||||
|
const renderComponent = () => {
|
||||||
|
return (
|
||||||
|
<Grid item {...gridProps}>
|
||||||
|
<Box
|
||||||
|
onClick={() => handleChange(value)}
|
||||||
|
sx={{
|
||||||
|
p: 4,
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
borderRadius: 1,
|
||||||
|
cursor: 'pointer',
|
||||||
|
position: 'relative',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
border: theme => `1px solid ${theme.palette.divider}`,
|
||||||
|
...(selected.includes(value)
|
||||||
|
? { borderColor: `${color}.main`, '& svg': { color: 'primary.main' } }
|
||||||
|
: { '&:hover': { borderColor: theme => `rgba(${theme.palette.customColors.main}, 0.25)` } })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon ? <Icon icon={icon} {...iconProps} /> : null}
|
||||||
|
{title ? (
|
||||||
|
typeof title === 'string' ? (
|
||||||
|
<Typography sx={{ fontWeight: 500, ...(content ? { mb: 2 } : { my: 'auto' }) }}>{title}</Typography>
|
||||||
|
) : (
|
||||||
|
title
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
{content ? (
|
||||||
|
typeof content === 'string' ? (
|
||||||
|
<Typography variant='body2' sx={{ my: 'auto', textAlign: 'center' }}>
|
||||||
|
{content}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
<Checkbox
|
||||||
|
size='small'
|
||||||
|
color={color}
|
||||||
|
name={`${name}-${value}`}
|
||||||
|
checked={selected.includes(value)}
|
||||||
|
onChange={() => handleChange(value)}
|
||||||
|
sx={{ mb: -2, ...(!icon && !title && !content && { mt: -2 }) }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data ? renderComponent() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomCheckboxIcons
|
||||||
63
src/@core/components/custom-checkbox/image/index.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Grid from '@mui/material/Grid'
|
||||||
|
import Checkbox from '@mui/material/Checkbox'
|
||||||
|
|
||||||
|
// ** Type Imports
|
||||||
|
import { CustomCheckboxImgProps } from 'src/@core/components/custom-checkbox/types'
|
||||||
|
|
||||||
|
const CustomCheckboxImg = (props: CustomCheckboxImgProps) => {
|
||||||
|
// ** Props
|
||||||
|
const { data, name, selected, gridProps, handleChange, color = 'primary' } = props
|
||||||
|
|
||||||
|
const { alt, img, value } = data
|
||||||
|
|
||||||
|
const renderComponent = () => {
|
||||||
|
return (
|
||||||
|
<Grid item {...gridProps}>
|
||||||
|
<Box
|
||||||
|
onClick={() => handleChange(value)}
|
||||||
|
sx={{
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
borderRadius: 1,
|
||||||
|
cursor: 'pointer',
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
border: theme => `2px solid ${theme.palette.divider}`,
|
||||||
|
'& img': {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover'
|
||||||
|
},
|
||||||
|
...(selected.includes(value)
|
||||||
|
? { borderColor: `${color}.main` }
|
||||||
|
: {
|
||||||
|
'&:hover': { borderColor: theme => `rgba(${theme.palette.customColors.main}, 0.25)` },
|
||||||
|
'&:not(:hover)': {
|
||||||
|
'& .MuiCheckbox-root': { display: 'none' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{typeof img === 'string' ? <img src={img} alt={alt ?? `checkbox-image-${value}`} /> : img}
|
||||||
|
<Checkbox
|
||||||
|
size='small'
|
||||||
|
color={color}
|
||||||
|
name={`${name}-${value}`}
|
||||||
|
checked={selected.includes(value)}
|
||||||
|
onChange={() => handleChange(value)}
|
||||||
|
sx={{ top: 4, right: 4, position: 'absolute' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data ? renderComponent() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomCheckboxImg
|
||||||
71
src/@core/components/custom-checkbox/types.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
// ** MUI Imports
|
||||||
|
import { GridProps } from '@mui/material/Grid'
|
||||||
|
|
||||||
|
// ** Type Imports
|
||||||
|
import { IconProps } from '@iconify/react'
|
||||||
|
import { ThemeColor } from 'src/@core/layouts/types'
|
||||||
|
|
||||||
|
// ** Types of Basic Custom Checkboxes
|
||||||
|
export type CustomCheckboxBasicData = {
|
||||||
|
value: string
|
||||||
|
content?: ReactNode
|
||||||
|
isSelected?: boolean
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
meta: ReactNode
|
||||||
|
title: ReactNode
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
meta?: never
|
||||||
|
title?: never
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
title: ReactNode
|
||||||
|
meta?: never
|
||||||
|
}
|
||||||
|
)
|
||||||
|
export type CustomCheckboxBasicProps = {
|
||||||
|
name: string
|
||||||
|
color?: ThemeColor
|
||||||
|
selected: string[]
|
||||||
|
gridProps: GridProps
|
||||||
|
data: CustomCheckboxBasicData
|
||||||
|
handleChange: (value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// ** Types of Custom Checkboxes with Icons
|
||||||
|
export type CustomCheckboxIconsData = {
|
||||||
|
value: string
|
||||||
|
title?: ReactNode
|
||||||
|
content?: ReactNode
|
||||||
|
isSelected?: boolean
|
||||||
|
}
|
||||||
|
export type CustomCheckboxIconsProps = {
|
||||||
|
name: string
|
||||||
|
icon?: string
|
||||||
|
color?: ThemeColor
|
||||||
|
selected: string[]
|
||||||
|
gridProps: GridProps
|
||||||
|
data: CustomCheckboxIconsData
|
||||||
|
iconProps?: Omit<IconProps, 'icon'>
|
||||||
|
handleChange: (value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// ** Types of Custom Checkboxes with Images
|
||||||
|
export type CustomCheckboxImgData = {
|
||||||
|
alt?: string
|
||||||
|
value: string
|
||||||
|
img: ReactNode
|
||||||
|
isSelected?: boolean
|
||||||
|
}
|
||||||
|
export type CustomCheckboxImgProps = {
|
||||||
|
name: string
|
||||||
|
color?: ThemeColor
|
||||||
|
selected: string[]
|
||||||
|
gridProps: GridProps
|
||||||
|
data: CustomCheckboxImgData
|
||||||
|
handleChange: (value: string) => void
|
||||||
|
}
|
||||||
95
src/@core/components/custom-radio/basic/index.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Grid from '@mui/material/Grid'
|
||||||
|
import Radio from '@mui/material/Radio'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
|
||||||
|
// ** Type Imports
|
||||||
|
import { CustomRadioBasicProps } from 'src/@core/components/custom-radio/types'
|
||||||
|
|
||||||
|
const CustomRadioBasic = (props: CustomRadioBasicProps) => {
|
||||||
|
// ** Props
|
||||||
|
const { name, data, selected, gridProps, handleChange, color = 'primary' } = props
|
||||||
|
|
||||||
|
const { meta, title, value, content } = data
|
||||||
|
|
||||||
|
const renderData = () => {
|
||||||
|
if (meta && title && content) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mb: 2,
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{typeof title === 'string' ? <Typography sx={{ mr: 2, fontWeight: 500 }}>{title}</Typography> : title}
|
||||||
|
{typeof meta === 'string' ? <Typography sx={{ color: 'text.disabled' }}>{meta}</Typography> : meta}
|
||||||
|
</Box>
|
||||||
|
{typeof content === 'string' ? <Typography variant='body2'>{content}</Typography> : content}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
} else if (meta && title && !content) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: '100%', display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' }}>
|
||||||
|
{typeof title === 'string' ? <Typography sx={{ mr: 2, fontWeight: 500 }}>{title}</Typography> : title}
|
||||||
|
{typeof meta === 'string' ? <Typography sx={{ color: 'text.disabled' }}>{meta}</Typography> : meta}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
} else if (!meta && title && content) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
{typeof title === 'string' ? <Typography sx={{ mb: 1, fontWeight: 500 }}>{title}</Typography> : title}
|
||||||
|
{typeof content === 'string' ? <Typography variant='body2'>{content}</Typography> : content}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
} else if (!meta && !title && content) {
|
||||||
|
return typeof content === 'string' ? <Typography variant='body2'>{content}</Typography> : content
|
||||||
|
} else if (!meta && title && !content) {
|
||||||
|
return typeof title === 'string' ? <Typography sx={{ fontWeight: 500 }}>{title}</Typography> : title
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderComponent = () => {
|
||||||
|
return (
|
||||||
|
<Grid item {...gridProps}>
|
||||||
|
<Box
|
||||||
|
onClick={() => handleChange(value)}
|
||||||
|
sx={{
|
||||||
|
p: 4,
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
borderRadius: 1,
|
||||||
|
cursor: 'pointer',
|
||||||
|
position: 'relative',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
border: theme => `1px solid ${theme.palette.divider}`,
|
||||||
|
...(selected === value
|
||||||
|
? { borderColor: `${color}.main` }
|
||||||
|
: { '&:hover': { borderColor: theme => `rgba(${theme.palette.customColors.main}, 0.25)` } })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Radio
|
||||||
|
name={name}
|
||||||
|
size='small'
|
||||||
|
color={color}
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
checked={selected === value}
|
||||||
|
sx={{ mb: -2, mt: -2.5, ml: -2.75 }}
|
||||||
|
/>
|
||||||
|
{renderData()}
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data ? renderComponent() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomRadioBasic
|
||||||
73
src/@core/components/custom-radio/icons/index.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Grid from '@mui/material/Grid'
|
||||||
|
import Radio from '@mui/material/Radio'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
|
||||||
|
// ** Type Imports
|
||||||
|
import { CustomRadioIconsProps } from 'src/@core/components/custom-radio/types'
|
||||||
|
|
||||||
|
// ** Icon Imports
|
||||||
|
import Icon from 'src/@core/components/icon'
|
||||||
|
|
||||||
|
const CustomRadioIcons = (props: CustomRadioIconsProps) => {
|
||||||
|
// ** Props
|
||||||
|
const { data, icon, name, selected, gridProps, iconProps, handleChange, color = 'primary' } = props
|
||||||
|
|
||||||
|
const { title, value, content } = data
|
||||||
|
|
||||||
|
const renderComponent = () => {
|
||||||
|
return (
|
||||||
|
<Grid item {...gridProps}>
|
||||||
|
<Box
|
||||||
|
onClick={() => handleChange(value)}
|
||||||
|
sx={{
|
||||||
|
p: 4,
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
borderRadius: 1,
|
||||||
|
cursor: 'pointer',
|
||||||
|
position: 'relative',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
border: theme => `1px solid ${theme.palette.divider}`,
|
||||||
|
...(selected === value
|
||||||
|
? { borderColor: `${color}.main`, '& svg': { color: 'primary.main' } }
|
||||||
|
: { '&:hover': { borderColor: theme => `rgba(${theme.palette.customColors.main}, 0.25)` } })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon ? <Icon icon={icon} {...iconProps} /> : null}
|
||||||
|
{title ? (
|
||||||
|
typeof title === 'string' ? (
|
||||||
|
<Typography sx={{ fontWeight: 500, ...(content ? { mb: 2 } : { my: 'auto' }) }}>{title}</Typography>
|
||||||
|
) : (
|
||||||
|
title
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
{content ? (
|
||||||
|
typeof content === 'string' ? (
|
||||||
|
<Typography variant='body2' sx={{ my: 'auto', textAlign: 'center' }}>
|
||||||
|
{content}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
<Radio
|
||||||
|
name={name}
|
||||||
|
size='small'
|
||||||
|
color={color}
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
checked={selected === value}
|
||||||
|
sx={{ mb: -2, ...(!icon && !title && !content && { mt: -2 }) }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data ? renderComponent() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomRadioIcons
|
||||||
58
src/@core/components/custom-radio/image/index.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Grid from '@mui/material/Grid'
|
||||||
|
import Radio from '@mui/material/Radio'
|
||||||
|
|
||||||
|
// ** Type Imports
|
||||||
|
import { CustomRadioImgProps } from 'src/@core/components/custom-radio/types'
|
||||||
|
|
||||||
|
const CustomRadioImg = (props: CustomRadioImgProps) => {
|
||||||
|
// ** Props
|
||||||
|
const { name, data, selected, gridProps, handleChange, color = 'primary' } = props
|
||||||
|
|
||||||
|
const { alt, img, value } = data
|
||||||
|
|
||||||
|
const renderComponent = () => {
|
||||||
|
return (
|
||||||
|
<Grid item {...gridProps}>
|
||||||
|
<Box
|
||||||
|
onClick={() => handleChange(value)}
|
||||||
|
sx={{
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
borderRadius: 1,
|
||||||
|
cursor: 'pointer',
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
border: theme => `2px solid ${theme.palette.divider}`,
|
||||||
|
...(selected === value
|
||||||
|
? { borderColor: `${color}.main` }
|
||||||
|
: { '&:hover': { borderColor: theme => `rgba(${theme.palette.customColors.main}, 0.25)` } }),
|
||||||
|
'& img': {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{typeof img === 'string' ? <img src={img} alt={alt ?? `radio-image-${value}`} /> : img}
|
||||||
|
<Radio
|
||||||
|
name={name}
|
||||||
|
size='small'
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
checked={selected === value}
|
||||||
|
sx={{ zIndex: -1, position: 'absolute', visibility: 'hidden' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data ? renderComponent() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomRadioImg
|
||||||
71
src/@core/components/custom-radio/types.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { ChangeEvent, ReactNode } from 'react'
|
||||||
|
|
||||||
|
// ** MUI Imports
|
||||||
|
import { GridProps } from '@mui/material/Grid'
|
||||||
|
|
||||||
|
// ** Type Imports
|
||||||
|
import { IconProps } from '@iconify/react'
|
||||||
|
import { ThemeColor } from 'src/@core/layouts/types'
|
||||||
|
|
||||||
|
// ** Types of Basic Custom Radios
|
||||||
|
export type CustomRadioBasicData = {
|
||||||
|
value: string
|
||||||
|
content?: ReactNode
|
||||||
|
isSelected?: boolean
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
meta: ReactNode
|
||||||
|
title: ReactNode
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
meta?: never
|
||||||
|
title?: never
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
title: ReactNode
|
||||||
|
meta?: never
|
||||||
|
}
|
||||||
|
)
|
||||||
|
export type CustomRadioBasicProps = {
|
||||||
|
name: string
|
||||||
|
selected: string
|
||||||
|
color?: ThemeColor
|
||||||
|
gridProps: GridProps
|
||||||
|
data: CustomRadioBasicData
|
||||||
|
handleChange: (prop: string | ChangeEvent<HTMLInputElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// ** Types of Custom Radios with Icons
|
||||||
|
export type CustomRadioIconsData = {
|
||||||
|
value: string
|
||||||
|
title?: ReactNode
|
||||||
|
content?: ReactNode
|
||||||
|
isSelected?: boolean
|
||||||
|
}
|
||||||
|
export type CustomRadioIconsProps = {
|
||||||
|
name: string
|
||||||
|
icon?: string
|
||||||
|
selected: string
|
||||||
|
color?: ThemeColor
|
||||||
|
gridProps: GridProps
|
||||||
|
data: CustomRadioIconsData
|
||||||
|
iconProps?: Omit<IconProps, 'icon'>
|
||||||
|
handleChange: (prop: string | ChangeEvent<HTMLInputElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// ** Types of Custom Radios with Images
|
||||||
|
export type CustomRadioImgData = {
|
||||||
|
alt?: string
|
||||||
|
value: string
|
||||||
|
img: ReactNode
|
||||||
|
isSelected?: boolean
|
||||||
|
}
|
||||||
|
export type CustomRadioImgProps = {
|
||||||
|
name: string
|
||||||
|
selected: string
|
||||||
|
color?: ThemeColor
|
||||||
|
gridProps: GridProps
|
||||||
|
data: CustomRadioImgData
|
||||||
|
handleChange: (prop: string | ChangeEvent<HTMLInputElement>) => void
|
||||||
|
}
|
||||||
401
src/@core/components/customizer/index.tsx
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
// ** Third Party Components
|
||||||
|
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||||
|
|
||||||
|
// ** MUI Imports
|
||||||
|
import Radio from '@mui/material/Radio'
|
||||||
|
import Switch from '@mui/material/Switch'
|
||||||
|
import Divider from '@mui/material/Divider'
|
||||||
|
import { styled } from '@mui/material/styles'
|
||||||
|
import IconButton from '@mui/material/IconButton'
|
||||||
|
import RadioGroup from '@mui/material/RadioGroup'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import Box, { BoxProps } from '@mui/material/Box'
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
|
import MuiDrawer, { DrawerProps } from '@mui/material/Drawer'
|
||||||
|
|
||||||
|
// ** Icon Imports
|
||||||
|
import Icon from 'src/@core/components/icon'
|
||||||
|
|
||||||
|
// ** Type Import
|
||||||
|
import { Settings } from 'src/@core/context/settingsContext'
|
||||||
|
|
||||||
|
// ** Hook Import
|
||||||
|
import { useSettings } from 'src/@core/hooks/useSettings'
|
||||||
|
|
||||||
|
const Toggler = styled(Box)<BoxProps>(({ theme }) => ({
|
||||||
|
right: 0,
|
||||||
|
top: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
cursor: 'pointer',
|
||||||
|
position: 'fixed',
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
zIndex: theme.zIndex.modal,
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
color: theme.palette.common.white,
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
borderTopLeftRadius: theme.shape.borderRadius,
|
||||||
|
borderBottomLeftRadius: theme.shape.borderRadius
|
||||||
|
}))
|
||||||
|
|
||||||
|
const Drawer = styled(MuiDrawer)<DrawerProps>(({ theme }) => ({
|
||||||
|
width: 400,
|
||||||
|
zIndex: theme.zIndex.modal,
|
||||||
|
'& .MuiFormControlLabel-root': {
|
||||||
|
marginRight: '0.6875rem'
|
||||||
|
},
|
||||||
|
'& .MuiDrawer-paper': {
|
||||||
|
border: 0,
|
||||||
|
width: 400,
|
||||||
|
zIndex: theme.zIndex.modal,
|
||||||
|
boxShadow: theme.shadows[9]
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const CustomizerSpacing = styled('div')(({ theme }) => ({
|
||||||
|
padding: theme.spacing(5, 6)
|
||||||
|
}))
|
||||||
|
|
||||||
|
const ColorBox = styled(Box)<BoxProps>(({ theme }) => ({
|
||||||
|
width: 45,
|
||||||
|
height: 45,
|
||||||
|
cursor: 'pointer',
|
||||||
|
margin: theme.spacing(2.5, 1.75, 1.75),
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
transition: 'margin .25s ease-in-out, width .25s ease-in-out, height .25s ease-in-out, box-shadow .25s ease-in-out',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: theme.shadows[4]
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const Customizer = () => {
|
||||||
|
// ** State
|
||||||
|
const [open, setOpen] = useState<boolean>(false)
|
||||||
|
|
||||||
|
// ** Hook
|
||||||
|
const { settings, saveSettings } = useSettings()
|
||||||
|
|
||||||
|
// ** Vars
|
||||||
|
const {
|
||||||
|
mode,
|
||||||
|
skin,
|
||||||
|
appBar,
|
||||||
|
footer,
|
||||||
|
layout,
|
||||||
|
navHidden,
|
||||||
|
direction,
|
||||||
|
appBarBlur,
|
||||||
|
themeColor,
|
||||||
|
navCollapsed,
|
||||||
|
contentWidth,
|
||||||
|
verticalNavToggleType
|
||||||
|
} = settings
|
||||||
|
|
||||||
|
const handleChange = (field: keyof Settings, value: Settings[keyof Settings]): void => {
|
||||||
|
saveSettings({ ...settings, [field]: value })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='customizer'>
|
||||||
|
<Toggler className='customizer-toggler' onClick={() => setOpen(true)}>
|
||||||
|
<Icon icon='tabler:settings' />
|
||||||
|
</Toggler>
|
||||||
|
<Drawer open={open} hideBackdrop anchor='right' variant='persistent'>
|
||||||
|
<Box
|
||||||
|
className='customizer-header'
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
p: theme => theme.spacing(3.5, 5),
|
||||||
|
borderBottom: theme => `1px solid ${theme.palette.divider}`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant='h6' sx={{ fontWeight: 600, textTransform: 'uppercase' }}>
|
||||||
|
Theme Customizer
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: 'text.secondary' }}>Customize & Preview in Real Time</Typography>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
sx={{
|
||||||
|
right: 20,
|
||||||
|
top: '50%',
|
||||||
|
position: 'absolute',
|
||||||
|
color: 'text.secondary',
|
||||||
|
transform: 'translateY(-50%)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon='tabler:x' fontSize={20} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<PerfectScrollbar options={{ wheelPropagation: false }}>
|
||||||
|
<CustomizerSpacing className='customizer-body'>
|
||||||
|
<Typography
|
||||||
|
component='p'
|
||||||
|
variant='caption'
|
||||||
|
sx={{ mb: 5, color: 'text.disabled', textTransform: 'uppercase' }}
|
||||||
|
>
|
||||||
|
Theming
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Skin */}
|
||||||
|
<Box sx={{ mb: 5 }}>
|
||||||
|
<Typography>Skin</Typography>
|
||||||
|
<RadioGroup
|
||||||
|
row
|
||||||
|
value={skin}
|
||||||
|
onChange={e => handleChange('skin', e.target.value as Settings['skin'])}
|
||||||
|
sx={{ '& .MuiFormControlLabel-label': { fontSize: '.875rem', color: 'text.secondary' } }}
|
||||||
|
>
|
||||||
|
<FormControlLabel value='default' label='Default' control={<Radio />} />
|
||||||
|
<FormControlLabel value='bordered' label='Bordered' control={<Radio />} />
|
||||||
|
</RadioGroup>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Mode */}
|
||||||
|
<Box sx={{ mb: 5 }}>
|
||||||
|
<Typography>Mode</Typography>
|
||||||
|
<RadioGroup
|
||||||
|
row
|
||||||
|
value={mode}
|
||||||
|
onChange={e => handleChange('mode', e.target.value as any)}
|
||||||
|
sx={{ '& .MuiFormControlLabel-label': { fontSize: '.875rem', color: 'text.secondary' } }}
|
||||||
|
>
|
||||||
|
<FormControlLabel value='light' label='Light' control={<Radio />} />
|
||||||
|
<FormControlLabel value='dark' label='Dark' control={<Radio />} />
|
||||||
|
{layout === 'horizontal' ? null : (
|
||||||
|
<FormControlLabel value='semi-dark' label='Semi Dark' control={<Radio />} />
|
||||||
|
)}
|
||||||
|
</RadioGroup>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Color Picker */}
|
||||||
|
<div>
|
||||||
|
<Typography>Primary Color</Typography>
|
||||||
|
<Box sx={{ display: 'flex' }}>
|
||||||
|
<ColorBox
|
||||||
|
onClick={() => handleChange('themeColor', 'primary')}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: '#7367F0',
|
||||||
|
...(themeColor === 'primary'
|
||||||
|
? { width: 53, height: 53, m: theme => theme.spacing(1.5, 0.75, 0) }
|
||||||
|
: {})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ColorBox
|
||||||
|
onClick={() => handleChange('themeColor', 'secondary')}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: 'secondary.main',
|
||||||
|
...(themeColor === 'secondary'
|
||||||
|
? { width: 53, height: 53, m: theme => theme.spacing(1.5, 0.75, 0) }
|
||||||
|
: {})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ColorBox
|
||||||
|
onClick={() => handleChange('themeColor', 'success')}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: 'success.main',
|
||||||
|
...(themeColor === 'success'
|
||||||
|
? { width: 53, height: 53, m: theme => theme.spacing(1.5, 0.75, 0) }
|
||||||
|
: {})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ColorBox
|
||||||
|
onClick={() => handleChange('themeColor', 'error')}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: 'error.main',
|
||||||
|
...(themeColor === 'error'
|
||||||
|
? { width: 53, height: 53, m: theme => theme.spacing(1.5, 0.75, 0) }
|
||||||
|
: {})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ColorBox
|
||||||
|
onClick={() => handleChange('themeColor', 'warning')}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: 'warning.main',
|
||||||
|
...(themeColor === 'warning'
|
||||||
|
? { width: 53, height: 53, m: theme => theme.spacing(1.5, 0.75, 0) }
|
||||||
|
: {})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ColorBox
|
||||||
|
onClick={() => handleChange('themeColor', 'info')}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: 'info.main',
|
||||||
|
...(themeColor === 'info' ? { width: 53, height: 53, m: theme => theme.spacing(1.5, 0.75, 0) } : {})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
</CustomizerSpacing>
|
||||||
|
|
||||||
|
<Divider sx={{ m: '0 !important' }} />
|
||||||
|
|
||||||
|
<CustomizerSpacing className='customizer-body'>
|
||||||
|
<Typography
|
||||||
|
component='p'
|
||||||
|
variant='caption'
|
||||||
|
sx={{ mb: 5, color: 'text.disabled', textTransform: 'uppercase' }}
|
||||||
|
>
|
||||||
|
Layout
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Content Width */}
|
||||||
|
<Box sx={{ mb: 5 }}>
|
||||||
|
<Typography>Content Width</Typography>
|
||||||
|
<RadioGroup
|
||||||
|
row
|
||||||
|
value={contentWidth}
|
||||||
|
onChange={e => handleChange('contentWidth', e.target.value as Settings['contentWidth'])}
|
||||||
|
sx={{ '& .MuiFormControlLabel-label': { fontSize: '.875rem', color: 'text.secondary' } }}
|
||||||
|
>
|
||||||
|
<FormControlLabel value='full' label='Full' control={<Radio />} />
|
||||||
|
<FormControlLabel value='boxed' label='Boxed' control={<Radio />} />
|
||||||
|
</RadioGroup>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* AppBar */}
|
||||||
|
<Box sx={{ mb: 5 }}>
|
||||||
|
<Typography>AppBar Type</Typography>
|
||||||
|
<RadioGroup
|
||||||
|
row
|
||||||
|
value={appBar}
|
||||||
|
onChange={e => handleChange('appBar', e.target.value as Settings['appBar'])}
|
||||||
|
sx={{ '& .MuiFormControlLabel-label': { fontSize: '.875rem', color: 'text.secondary' } }}
|
||||||
|
>
|
||||||
|
<FormControlLabel value='fixed' label='Fixed' control={<Radio />} />
|
||||||
|
<FormControlLabel value='static' label='Static' control={<Radio />} />
|
||||||
|
{layout === 'horizontal' ? null : (
|
||||||
|
<FormControlLabel value='hidden' label='Hidden' control={<Radio />} />
|
||||||
|
)}
|
||||||
|
</RadioGroup>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<Box sx={{ mb: 5 }}>
|
||||||
|
<Typography>Footer Type</Typography>
|
||||||
|
<RadioGroup
|
||||||
|
row
|
||||||
|
value={footer}
|
||||||
|
onChange={e => handleChange('footer', e.target.value as Settings['footer'])}
|
||||||
|
sx={{ '& .MuiFormControlLabel-label': { fontSize: '.875rem', color: 'text.secondary' } }}
|
||||||
|
>
|
||||||
|
<FormControlLabel value='fixed' label='Fixed' control={<Radio />} />
|
||||||
|
<FormControlLabel value='static' label='Static' control={<Radio />} />
|
||||||
|
<FormControlLabel value='hidden' label='Hidden' control={<Radio />} />
|
||||||
|
</RadioGroup>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* AppBar Blur */}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<Typography>AppBar Blur</Typography>
|
||||||
|
<Switch
|
||||||
|
name='appBarBlur'
|
||||||
|
checked={appBarBlur}
|
||||||
|
onChange={e => handleChange('appBarBlur', e.target.checked)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</CustomizerSpacing>
|
||||||
|
|
||||||
|
<Divider sx={{ m: '0 !important' }} />
|
||||||
|
|
||||||
|
<CustomizerSpacing className='customizer-body'>
|
||||||
|
<Typography
|
||||||
|
component='p'
|
||||||
|
variant='caption'
|
||||||
|
sx={{ mb: 5, color: 'text.disabled', textTransform: 'uppercase' }}
|
||||||
|
>
|
||||||
|
Menu
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Menu Layout */}
|
||||||
|
<Box sx={{ mb: layout === 'horizontal' && appBar === 'hidden' ? {} : 5 }}>
|
||||||
|
<Typography>Menu Layout</Typography>
|
||||||
|
<RadioGroup
|
||||||
|
row
|
||||||
|
value={layout}
|
||||||
|
onChange={e => {
|
||||||
|
saveSettings({
|
||||||
|
...settings,
|
||||||
|
layout: e.target.value as Settings['layout'],
|
||||||
|
lastLayout: e.target.value as Settings['lastLayout']
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
sx={{ '& .MuiFormControlLabel-label': { fontSize: '.875rem', color: 'text.secondary' } }}
|
||||||
|
>
|
||||||
|
<FormControlLabel value='vertical' label='Vertical' control={<Radio />} />
|
||||||
|
<FormControlLabel value='horizontal' label='Horizontal' control={<Radio />} />
|
||||||
|
</RadioGroup>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Menu Toggle */}
|
||||||
|
{navHidden || layout === 'horizontal' ? null : (
|
||||||
|
<Box sx={{ mb: 5 }}>
|
||||||
|
<Typography>Menu Toggle</Typography>
|
||||||
|
<RadioGroup
|
||||||
|
row
|
||||||
|
value={verticalNavToggleType}
|
||||||
|
onChange={e =>
|
||||||
|
handleChange('verticalNavToggleType', e.target.value as Settings['verticalNavToggleType'])
|
||||||
|
}
|
||||||
|
sx={{ '& .MuiFormControlLabel-label': { fontSize: '.875rem', color: 'text.secondary' } }}
|
||||||
|
>
|
||||||
|
<FormControlLabel value='accordion' label='Accordion' control={<Radio />} />
|
||||||
|
<FormControlLabel value='collapse' label='Collapse' control={<Radio />} />
|
||||||
|
</RadioGroup>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Menu Collapsed */}
|
||||||
|
{navHidden || layout === 'horizontal' ? null : (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 5 }}>
|
||||||
|
<Typography>Menu Collapsed</Typography>
|
||||||
|
<Switch
|
||||||
|
name='navCollapsed'
|
||||||
|
checked={navCollapsed}
|
||||||
|
onChange={e => handleChange('navCollapsed', e.target.checked)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Menu Hidden */}
|
||||||
|
{layout === 'horizontal' && appBar === 'hidden' ? null : (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<Typography>Menu Hidden</Typography>
|
||||||
|
<Switch
|
||||||
|
name='navHidden'
|
||||||
|
checked={navHidden}
|
||||||
|
onChange={e => handleChange('navHidden', e.target.checked)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</CustomizerSpacing>
|
||||||
|
|
||||||
|
<Divider sx={{ m: '0 !important' }} />
|
||||||
|
|
||||||
|
<CustomizerSpacing className='customizer-body'>
|
||||||
|
<Typography
|
||||||
|
component='p'
|
||||||
|
variant='caption'
|
||||||
|
sx={{ mb: 5, color: 'text.disabled', textTransform: 'uppercase' }}
|
||||||
|
>
|
||||||
|
Misc
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* RTL */}
|
||||||
|
<Box sx={{ mb: 5, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<Typography>RTL</Typography>
|
||||||
|
<Switch
|
||||||
|
name='direction'
|
||||||
|
checked={direction === 'rtl'}
|
||||||
|
onChange={e => handleChange('direction', e.target.checked ? 'rtl' : 'ltr')}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</CustomizerSpacing>
|
||||||
|
</PerfectScrollbar>
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Customizer
|
||||||
8
src/@core/components/icon/index.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// ** Icon Imports
|
||||||
|
import { Icon, IconProps } from '@iconify/react'
|
||||||
|
|
||||||
|
const IconifyIcon = ({ icon, ...rest }: IconProps) => {
|
||||||
|
return <Icon icon={icon} fontSize='1.375rem' {...rest} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IconifyIcon
|
||||||
57
src/@core/components/mui/avatar/index.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { forwardRef, Ref } from 'react'
|
||||||
|
|
||||||
|
// ** MUI Imports
|
||||||
|
import MuiAvatar from '@mui/material/Avatar'
|
||||||
|
import { lighten, useTheme } from '@mui/material/styles'
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { CustomAvatarProps } from './types'
|
||||||
|
import { ThemeColor } from 'src/@core/layouts/types'
|
||||||
|
|
||||||
|
// ** Hooks Imports
|
||||||
|
import useBgColor, { UseBgColorType } from 'src/@core/hooks/useBgColor'
|
||||||
|
|
||||||
|
const Avatar = forwardRef((props: CustomAvatarProps, ref: Ref<any>) => {
|
||||||
|
// ** Props
|
||||||
|
const { sx, src, skin, color } = props
|
||||||
|
|
||||||
|
// ** Hook
|
||||||
|
const theme = useTheme()
|
||||||
|
const bgColors: UseBgColorType = useBgColor()
|
||||||
|
|
||||||
|
const getAvatarStyles = (skin: 'filled' | 'light' | 'light-static' | undefined, skinColor: ThemeColor) => {
|
||||||
|
let avatarStyles
|
||||||
|
|
||||||
|
if (skin === 'light') {
|
||||||
|
avatarStyles = { ...bgColors[`${skinColor}Light`] }
|
||||||
|
} else if (skin === 'light-static') {
|
||||||
|
avatarStyles = {
|
||||||
|
color: bgColors[`${skinColor}Light`].color,
|
||||||
|
backgroundColor: lighten(theme.palette[skinColor].main, 0.88)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
avatarStyles = { ...bgColors[`${skinColor}Filled`] }
|
||||||
|
}
|
||||||
|
|
||||||
|
return avatarStyles
|
||||||
|
}
|
||||||
|
|
||||||
|
const colors: UseBgColorType = {
|
||||||
|
primary: getAvatarStyles(skin, 'primary'),
|
||||||
|
secondary: getAvatarStyles(skin, 'secondary'),
|
||||||
|
success: getAvatarStyles(skin, 'success'),
|
||||||
|
error: getAvatarStyles(skin, 'error'),
|
||||||
|
warning: getAvatarStyles(skin, 'warning'),
|
||||||
|
info: getAvatarStyles(skin, 'info')
|
||||||
|
}
|
||||||
|
|
||||||
|
return <MuiAvatar ref={ref} {...props} sx={!src && skin && color ? Object.assign(colors[color], sx) : sx} />
|
||||||
|
})
|
||||||
|
|
||||||
|
Avatar.defaultProps = {
|
||||||
|
skin: 'filled',
|
||||||
|
color: 'primary'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Avatar
|
||||||
10
src/@core/components/mui/avatar/types.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import { AvatarProps } from '@mui/material/Avatar'
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { ThemeColor } from 'src/@core/layouts/types'
|
||||||
|
|
||||||
|
export type CustomAvatarProps = AvatarProps & {
|
||||||
|
color?: ThemeColor
|
||||||
|
skin?: 'filled' | 'light' | 'light-static'
|
||||||
|
}
|
||||||
34
src/@core/components/mui/badge/index.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import MuiBadge from '@mui/material/Badge'
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { CustomBadgeProps } from './types'
|
||||||
|
|
||||||
|
// ** Hooks Imports
|
||||||
|
import useBgColor, { UseBgColorType } from 'src/@core/hooks/useBgColor'
|
||||||
|
|
||||||
|
const Badge = (props: CustomBadgeProps) => {
|
||||||
|
// ** Props
|
||||||
|
const { sx, skin, color } = props
|
||||||
|
|
||||||
|
// ** Hook
|
||||||
|
const bgColors = useBgColor()
|
||||||
|
|
||||||
|
const colors: UseBgColorType = {
|
||||||
|
primary: { ...bgColors.primaryLight },
|
||||||
|
secondary: { ...bgColors.secondaryLight },
|
||||||
|
success: { ...bgColors.successLight },
|
||||||
|
error: { ...bgColors.errorLight },
|
||||||
|
warning: { ...bgColors.warningLight },
|
||||||
|
info: { ...bgColors.infoLight }
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MuiBadge
|
||||||
|
{...props}
|
||||||
|
sx={skin === 'light' && color ? Object.assign({ '& .MuiBadge-badge': colors[color] }, sx) : sx}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Badge
|
||||||
4
src/@core/components/mui/badge/types.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import { BadgeProps } from '@mui/material/Badge'
|
||||||
|
|
||||||
|
export type CustomBadgeProps = BadgeProps & { skin?: 'light' }
|
||||||
46
src/@core/components/mui/chip/index.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import MuiChip from '@mui/material/Chip'
|
||||||
|
|
||||||
|
// ** Third Party Imports
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { CustomChipProps } from './types'
|
||||||
|
|
||||||
|
// ** Hooks Imports
|
||||||
|
import useBgColor, { UseBgColorType } from 'src/@core/hooks/useBgColor'
|
||||||
|
|
||||||
|
const Chip = (props: CustomChipProps) => {
|
||||||
|
// ** Props
|
||||||
|
const { sx, skin, color, rounded } = props
|
||||||
|
|
||||||
|
// ** Hook
|
||||||
|
const bgColors = useBgColor()
|
||||||
|
|
||||||
|
const colors: UseBgColorType = {
|
||||||
|
primary: { ...bgColors.primaryLight },
|
||||||
|
secondary: { ...bgColors.secondaryLight },
|
||||||
|
success: { ...bgColors.successLight },
|
||||||
|
error: { ...bgColors.errorLight },
|
||||||
|
warning: { ...bgColors.warningLight },
|
||||||
|
info: { ...bgColors.infoLight }
|
||||||
|
}
|
||||||
|
|
||||||
|
const propsToPass = { ...props }
|
||||||
|
|
||||||
|
propsToPass.rounded = undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MuiChip
|
||||||
|
{...propsToPass}
|
||||||
|
variant='filled'
|
||||||
|
className={clsx({
|
||||||
|
'MuiChip-rounded': rounded,
|
||||||
|
'MuiChip-light': skin === 'light'
|
||||||
|
})}
|
||||||
|
sx={skin === 'light' && color ? Object.assign(colors[color], sx) : sx}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Chip
|
||||||
4
src/@core/components/mui/chip/types.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import { ChipProps } from '@mui/material/Chip'
|
||||||
|
|
||||||
|
export type CustomChipProps = ChipProps & { skin?: 'light'; rounded?: boolean }
|
||||||
73
src/@core/components/mui/timeline-dot/index.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import { useTheme } from '@mui/material/styles'
|
||||||
|
import MuiTimelineDot from '@mui/lab/TimelineDot'
|
||||||
|
|
||||||
|
// ** Hooks Imports
|
||||||
|
import useBgColor, { UseBgColorType } from 'src/@core/hooks/useBgColor'
|
||||||
|
|
||||||
|
// ** Util Import
|
||||||
|
import { hexToRGBA } from 'src/@core/utils/hex-to-rgba'
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { CustomTimelineDotProps, ColorsType } from './types'
|
||||||
|
|
||||||
|
const TimelineDot = (props: CustomTimelineDotProps) => {
|
||||||
|
// ** Props
|
||||||
|
const { sx, skin, color, variant } = props
|
||||||
|
|
||||||
|
// ** Hook
|
||||||
|
const theme = useTheme()
|
||||||
|
const bgColors: UseBgColorType = useBgColor()
|
||||||
|
|
||||||
|
const colors: ColorsType = {
|
||||||
|
primary: {
|
||||||
|
boxShadow: 'none',
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
backgroundColor: bgColors.primaryLight.backgroundColor
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
boxShadow: 'none',
|
||||||
|
color: theme.palette.secondary.main,
|
||||||
|
backgroundColor: bgColors.secondaryLight.backgroundColor
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
boxShadow: 'none',
|
||||||
|
color: theme.palette.success.main,
|
||||||
|
backgroundColor: bgColors.successLight.backgroundColor
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
boxShadow: 'none',
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
backgroundColor: bgColors.errorLight.backgroundColor
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
boxShadow: 'none',
|
||||||
|
color: theme.palette.warning.main,
|
||||||
|
backgroundColor: bgColors.warningLight.backgroundColor
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
boxShadow: 'none',
|
||||||
|
color: theme.palette.info.main,
|
||||||
|
backgroundColor: bgColors.infoLight.backgroundColor
|
||||||
|
},
|
||||||
|
grey: {
|
||||||
|
boxShadow: 'none',
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
backgroundColor: hexToRGBA(theme.palette.grey[500], 0.12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MuiTimelineDot
|
||||||
|
{...props}
|
||||||
|
sx={color && skin === 'light' && variant === 'filled' ? Object.assign(colors[color], sx) : sx}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TimelineDot.defaultProps = {
|
||||||
|
color: 'grey',
|
||||||
|
variant: 'filled'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TimelineDot
|
||||||
12
src/@core/components/mui/timeline-dot/types.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import { TimelineDotProps } from '@mui/lab/TimelineDot'
|
||||||
|
|
||||||
|
export type CustomTimelineDotProps = TimelineDotProps & { skin?: 'light' }
|
||||||
|
|
||||||
|
export type ColorsType = {
|
||||||
|
[key: string]: {
|
||||||
|
color: string
|
||||||
|
boxShadow: string
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
}
|
||||||
116
src/@core/components/option-menu/index.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { MouseEvent, useState, ReactNode } from 'react'
|
||||||
|
|
||||||
|
// ** Next Import
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
// ** MUI Imports
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Menu from '@mui/material/Menu'
|
||||||
|
import Divider from '@mui/material/Divider'
|
||||||
|
import MenuItem from '@mui/material/MenuItem'
|
||||||
|
import IconButton from '@mui/material/IconButton'
|
||||||
|
|
||||||
|
// ** Icon Imports
|
||||||
|
import Icon from 'src/@core/components/icon'
|
||||||
|
|
||||||
|
// ** Type Imports
|
||||||
|
import { OptionType, OptionsMenuType, OptionMenuItemType } from './types'
|
||||||
|
|
||||||
|
// ** Hook Import
|
||||||
|
import { useSettings } from 'src/@core/hooks/useSettings'
|
||||||
|
|
||||||
|
const MenuItemWrapper = ({ children, option }: { children: ReactNode; option: OptionMenuItemType }) => {
|
||||||
|
if (option.href) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
component={Link}
|
||||||
|
href={option.href}
|
||||||
|
{...option.linkProps}
|
||||||
|
sx={{
|
||||||
|
px: 4,
|
||||||
|
py: 1.5,
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
color: 'inherit',
|
||||||
|
alignItems: 'center',
|
||||||
|
textDecoration: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const OptionsMenu = (props: OptionsMenuType) => {
|
||||||
|
// ** Props
|
||||||
|
const { icon, options, menuProps, iconProps, leftAlignMenu, iconButtonProps } = props
|
||||||
|
|
||||||
|
// ** State
|
||||||
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
||||||
|
|
||||||
|
// ** Hook & Var
|
||||||
|
const { settings } = useSettings()
|
||||||
|
const { direction } = settings
|
||||||
|
|
||||||
|
const handleClick = (event: MouseEvent<HTMLElement>) => {
|
||||||
|
setAnchorEl(event.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IconButton aria-haspopup='true' onClick={handleClick} {...iconButtonProps}>
|
||||||
|
{icon ? icon : <Icon icon='tabler:dots-vertical' {...iconProps} />}
|
||||||
|
</IconButton>
|
||||||
|
<Menu
|
||||||
|
keepMounted
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={handleClose}
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
|
{...(!leftAlignMenu && {
|
||||||
|
anchorOrigin: { vertical: 'bottom', horizontal: direction === 'ltr' ? 'right' : 'left' },
|
||||||
|
transformOrigin: { vertical: 'top', horizontal: direction === 'ltr' ? 'right' : 'left' }
|
||||||
|
})}
|
||||||
|
{...menuProps}
|
||||||
|
>
|
||||||
|
{options.map((option: OptionType, index: number) => {
|
||||||
|
if (typeof option === 'string') {
|
||||||
|
return (
|
||||||
|
<MenuItem key={index} onClick={handleClose}>
|
||||||
|
{option}
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
} else if ('divider' in option) {
|
||||||
|
return option.divider && <Divider key={index} {...option.dividerProps} />
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
key={index}
|
||||||
|
{...option.menuItemProps}
|
||||||
|
{...(option.href && { sx: { p: 0 } })}
|
||||||
|
onClick={e => {
|
||||||
|
handleClose()
|
||||||
|
option.menuItemProps && option.menuItemProps.onClick ? option.menuItemProps.onClick(e) : null
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItemWrapper option={option}>
|
||||||
|
{option.icon ? option.icon : null}
|
||||||
|
{option.text}
|
||||||
|
</MenuItemWrapper>
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OptionsMenu
|
||||||
42
src/@core/components/option-menu/types.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// ** React Import
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
// ** MUI Imports
|
||||||
|
import { MenuProps } from '@mui/material/Menu'
|
||||||
|
import { DividerProps } from '@mui/material/Divider'
|
||||||
|
import { MenuItemProps } from '@mui/material/MenuItem'
|
||||||
|
import { IconButtonProps } from '@mui/material/IconButton'
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { LinkProps } from 'next/link'
|
||||||
|
import { IconProps } from '@iconify/react'
|
||||||
|
|
||||||
|
export type OptionDividerType = {
|
||||||
|
divider: boolean
|
||||||
|
dividerProps?: DividerProps
|
||||||
|
href?: never
|
||||||
|
icon?: never
|
||||||
|
text?: never
|
||||||
|
linkProps?: never
|
||||||
|
menuItemProps?: never
|
||||||
|
}
|
||||||
|
export type OptionMenuItemType = {
|
||||||
|
text: ReactNode
|
||||||
|
icon?: ReactNode
|
||||||
|
linkProps?: LinkProps
|
||||||
|
href?: LinkProps['href']
|
||||||
|
menuItemProps?: MenuItemProps
|
||||||
|
divider?: never
|
||||||
|
dividerProps?: never
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OptionType = string | OptionDividerType | OptionMenuItemType
|
||||||
|
|
||||||
|
export type OptionsMenuType = {
|
||||||
|
icon?: ReactNode
|
||||||
|
options: OptionType[]
|
||||||
|
leftAlignMenu?: boolean
|
||||||
|
iconButtonProps?: IconButtonProps
|
||||||
|
iconProps?: Omit<IconProps, 'icon'>
|
||||||
|
menuProps?: Omit<MenuProps, 'open'>
|
||||||
|
}
|
||||||
19
src/@core/components/page-header/index.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import Grid from '@mui/material/Grid'
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { PageHeaderProps } from './types'
|
||||||
|
|
||||||
|
const PageHeader = (props: PageHeaderProps) => {
|
||||||
|
// ** Props
|
||||||
|
const { title, subtitle } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
{title}
|
||||||
|
{subtitle || null}
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageHeader
|
||||||
6
src/@core/components/page-header/types.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
export type PageHeaderProps = {
|
||||||
|
title: ReactNode
|
||||||
|
subtitle?: ReactNode
|
||||||
|
}
|
||||||
121
src/@core/components/plan-details/index.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import { styled } from '@mui/material/styles'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import Box, { BoxProps } from '@mui/material/Box'
|
||||||
|
|
||||||
|
// ** Icon Imports
|
||||||
|
import Icon from 'src/@core/components/icon'
|
||||||
|
|
||||||
|
// ** Util Import
|
||||||
|
import { hexToRGBA } from 'src/@core/utils/hex-to-rgba'
|
||||||
|
|
||||||
|
// ** Custom Components Imports
|
||||||
|
import CustomChip from 'src/@core/components/mui/chip'
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { PricingPlanProps } from './types'
|
||||||
|
|
||||||
|
// ** Styled Component for the wrapper of whole component
|
||||||
|
const BoxWrapper = styled(Box)<BoxProps>(({ theme }) => ({
|
||||||
|
position: 'relative',
|
||||||
|
padding: theme.spacing(6),
|
||||||
|
paddingTop: theme.spacing(16),
|
||||||
|
borderRadius: theme.shape.borderRadius
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ** Styled Component for the wrapper of all the features of a plan
|
||||||
|
const BoxFeature = styled(Box)<BoxProps>(({ theme }) => ({
|
||||||
|
marginBottom: theme.spacing(4),
|
||||||
|
'& > :not(:last-child)': {
|
||||||
|
marginBottom: theme.spacing(2.5)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const PlanDetails = (props: PricingPlanProps) => {
|
||||||
|
// ** Props
|
||||||
|
const { plan, data } = props
|
||||||
|
|
||||||
|
const renderFeatures = () => {
|
||||||
|
return data?.planBenefits.map((item: string, index: number) => (
|
||||||
|
<Box key={index} sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Box component='span' sx={{ display: 'inline-flex', color: 'text.secondary', mr: 2.5 }}>
|
||||||
|
<Icon icon='tabler:circle' fontSize='0.875rem' />
|
||||||
|
</Box>
|
||||||
|
<Typography sx={{ color: 'text.secondary' }}>{item}</Typography>
|
||||||
|
</Box>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BoxWrapper
|
||||||
|
sx={{
|
||||||
|
border: theme =>
|
||||||
|
!data?.popularPlan
|
||||||
|
? `1px solid ${theme.palette.divider}`
|
||||||
|
: `1px solid ${hexToRGBA(theme.palette.primary.main, 0.5)}`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data?.popularPlan ? (
|
||||||
|
<CustomChip
|
||||||
|
rounded
|
||||||
|
size='small'
|
||||||
|
skin='light'
|
||||||
|
label='Popular'
|
||||||
|
color='primary'
|
||||||
|
sx={{
|
||||||
|
top: 24,
|
||||||
|
right: 24,
|
||||||
|
position: 'absolute',
|
||||||
|
'& .MuiChip-label': {
|
||||||
|
px: 1.75,
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '0.75rem'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<Box sx={{ mb: 4, display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<img
|
||||||
|
width={data?.imgWidth}
|
||||||
|
src={`${data?.imgSrc}`}
|
||||||
|
height={data?.imgHeight}
|
||||||
|
alt={`${data?.title.toLowerCase().replace(' ', '-')}-plan-img`}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ textAlign: 'center' }}>
|
||||||
|
<Typography sx={{ mb: 1.5, fontWeight: 500, lineHeight: 1.385, fontSize: '1.625rem' }}>
|
||||||
|
{data?.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: 'text.secondary' }}>{data?.subtitle}</Typography>
|
||||||
|
<Box sx={{ my: 7, position: 'relative' }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Typography sx={{ mt: 2.5, mr: 0.5, fontWeight: 500, color: 'primary.main', alignSelf: 'flex-start' }}>
|
||||||
|
$
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='h3' sx={{ fontWeight: 500, color: 'primary.main' }}>
|
||||||
|
{plan === 'monthly' ? data?.monthlyPrice : data?.yearlyPlan.perMonth}
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ mb: 1.5, alignSelf: 'flex-end', color: 'text.disabled' }}>/month</Typography>
|
||||||
|
</Box>
|
||||||
|
{plan !== 'monthly' && data?.monthlyPrice !== 0 ? (
|
||||||
|
<Typography
|
||||||
|
variant='body2'
|
||||||
|
sx={{ top: 52, left: '50%', position: 'absolute', color: 'text.disabled', transform: 'translateX(-50%)' }}
|
||||||
|
>{`USD ${data?.yearlyPlan.totalAnnual}/year`}</Typography>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<BoxFeature>{renderFeatures()}</BoxFeature>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
color={data?.currentPlan ? 'success' : 'primary'}
|
||||||
|
variant={data?.popularPlan ? 'contained' : 'outlined'}
|
||||||
|
>
|
||||||
|
{data?.currentPlan ? 'Your Current Plan' : 'Upgrade'}
|
||||||
|
</Button>
|
||||||
|
</BoxWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PlanDetails
|
||||||
39
src/@core/components/plan-details/types.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
export type PricingPlanType = {
|
||||||
|
title: string
|
||||||
|
imgSrc: string
|
||||||
|
subtitle: string
|
||||||
|
imgWidth?: number
|
||||||
|
imgHeight?: number
|
||||||
|
currentPlan: boolean
|
||||||
|
popularPlan: boolean
|
||||||
|
monthlyPrice: number
|
||||||
|
planBenefits: string[]
|
||||||
|
yearlyPlan: {
|
||||||
|
perMonth: number
|
||||||
|
totalAnnual: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PricingPlanProps = {
|
||||||
|
plan: string
|
||||||
|
data?: PricingPlanType
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PricingFaqType = {
|
||||||
|
id: string
|
||||||
|
answer: string
|
||||||
|
question: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PricingTableRowType = { feature: string; starter: boolean; pro: boolean | string; enterprise: boolean }
|
||||||
|
|
||||||
|
export type PricingTableType = {
|
||||||
|
header: { title: string; subtitle: string; isPro?: boolean }[]
|
||||||
|
rows: PricingTableRowType[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PricingDataType = {
|
||||||
|
faq: PricingFaqType[]
|
||||||
|
pricingTable: PricingTableType
|
||||||
|
pricingPlans: PricingPlanType[]
|
||||||
|
}
|
||||||
7
src/@core/components/react-apexcharts/index.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// ** Next Import
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
|
||||||
|
// ! To avoid 'Window is not defined' error
|
||||||
|
const ReactApexcharts = dynamic(() => import('react-apexcharts'), { ssr: false })
|
||||||
|
|
||||||
|
export default ReactApexcharts
|
||||||
12
src/@core/components/react-draft-wysiwyg/index.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// ** Next Import
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { EditorProps } from 'react-draft-wysiwyg'
|
||||||
|
|
||||||
|
// ! To avoid 'Window is not defined' error
|
||||||
|
const ReactDraftWysiwyg = dynamic<EditorProps>(() => import('react-draft-wysiwyg').then(mod => mod.Editor), {
|
||||||
|
ssr: false
|
||||||
|
})
|
||||||
|
|
||||||
|
export default ReactDraftWysiwyg
|
||||||
22
src/@core/components/repeater/index.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// ** Types
|
||||||
|
import { RepeaterProps } from './types'
|
||||||
|
|
||||||
|
const Repeater = (props: RepeaterProps) => {
|
||||||
|
// ** Props
|
||||||
|
const { count, tag, children } = props
|
||||||
|
|
||||||
|
// ** Custom Tag
|
||||||
|
const Tag = tag || 'div'
|
||||||
|
|
||||||
|
// ** Default Items
|
||||||
|
const items = []
|
||||||
|
|
||||||
|
// ** Loop passed count times and push it in items Array
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
items.push(children(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Tag {...props}>{items}</Tag>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Repeater
|
||||||
8
src/@core/components/repeater/types.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { ReactNode, ComponentType } from 'react'
|
||||||
|
|
||||||
|
export type RepeaterProps = {
|
||||||
|
count: number
|
||||||
|
children(i: number): ReactNode
|
||||||
|
tag?: ComponentType | keyof JSX.IntrinsicElements
|
||||||
|
}
|
||||||
47
src/@core/components/scroll-to-top/index.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
// ** MUI Imports
|
||||||
|
import Zoom from '@mui/material/Zoom'
|
||||||
|
import { styled } from '@mui/material/styles'
|
||||||
|
import useScrollTrigger from '@mui/material/useScrollTrigger'
|
||||||
|
|
||||||
|
interface ScrollToTopProps {
|
||||||
|
className?: string
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScrollToTopStyled = styled('div')(({ theme }) => ({
|
||||||
|
zIndex: 11,
|
||||||
|
position: 'fixed',
|
||||||
|
right: theme.spacing(6),
|
||||||
|
bottom: theme.spacing(10)
|
||||||
|
}))
|
||||||
|
|
||||||
|
const ScrollToTop = (props: ScrollToTopProps) => {
|
||||||
|
// ** Props
|
||||||
|
const { children, className } = props
|
||||||
|
|
||||||
|
// ** init trigger
|
||||||
|
const trigger = useScrollTrigger({
|
||||||
|
threshold: 400,
|
||||||
|
disableHysteresis: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
const anchor = document.querySelector('body')
|
||||||
|
if (anchor) {
|
||||||
|
anchor.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Zoom in={trigger}>
|
||||||
|
<ScrollToTopStyled className={className} onClick={handleClick} role='presentation'>
|
||||||
|
{children}
|
||||||
|
</ScrollToTopStyled>
|
||||||
|
</Zoom>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ScrollToTop
|
||||||
65
src/@core/components/sidebar/index.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { Fragment, useEffect } from 'react'
|
||||||
|
|
||||||
|
// ** MUI Imports
|
||||||
|
import Backdrop from '@mui/material/Backdrop'
|
||||||
|
import Box, { BoxProps } from '@mui/material/Box'
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { SidebarType } from './type'
|
||||||
|
|
||||||
|
const Sidebar = (props: BoxProps & SidebarType) => {
|
||||||
|
// ** Props
|
||||||
|
const { sx, show, direction, children, hideBackdrop, onOpen, onClose, backDropClick } = props
|
||||||
|
|
||||||
|
const handleBackdropClick = () => {
|
||||||
|
if (backDropClick) {
|
||||||
|
backDropClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (show && onOpen) {
|
||||||
|
onOpen()
|
||||||
|
}
|
||||||
|
if (show === false && onClose) {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}, [onClose, onOpen, show])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
top: 0,
|
||||||
|
height: '100%',
|
||||||
|
zIndex: 'drawer',
|
||||||
|
position: 'absolute',
|
||||||
|
transition: 'all 0.25s ease-in-out',
|
||||||
|
backgroundColor: 'background.paper',
|
||||||
|
...(show ? { opacity: 1 } : { opacity: 0 }),
|
||||||
|
...(direction === 'right'
|
||||||
|
? { left: 'auto', right: show ? 0 : '-100%' }
|
||||||
|
: { right: 'auto', left: show ? 0 : '-100%' }),
|
||||||
|
...sx
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
{hideBackdrop ? null : (
|
||||||
|
<Backdrop
|
||||||
|
open={show}
|
||||||
|
transitionDuration={250}
|
||||||
|
onClick={handleBackdropClick}
|
||||||
|
sx={{ position: 'absolute', zIndex: theme => theme.zIndex.drawer - 1 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Sidebar
|
||||||
|
|
||||||
|
Sidebar.defaultProps = {
|
||||||
|
direction: 'left'
|
||||||
|
}
|
||||||
12
src/@core/components/sidebar/type.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
export type SidebarType = {
|
||||||
|
show: boolean
|
||||||
|
onOpen?: () => void
|
||||||
|
children: ReactNode
|
||||||
|
onClose?: () => void
|
||||||
|
hideBackdrop?: boolean
|
||||||
|
backDropClick?: () => void
|
||||||
|
direction?: 'left' | 'right'
|
||||||
|
}
|
||||||
54
src/@core/components/spinner/index.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import { useTheme } from '@mui/material/styles'
|
||||||
|
import Box, { BoxProps } from '@mui/material/Box'
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress'
|
||||||
|
|
||||||
|
const FallbackSpinner = ({ sx }: { sx?: BoxProps['sx'] }) => {
|
||||||
|
// ** Hook
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
...sx
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width={82} height={56.375} viewBox='0 0 32 22' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||||
|
<path
|
||||||
|
fillRule='evenodd'
|
||||||
|
clipRule='evenodd'
|
||||||
|
fill={theme.palette.primary.main}
|
||||||
|
d='M0.00172773 0V6.85398C0.00172773 6.85398 -0.133178 9.01207 1.98092 10.8388L13.6912 21.9964L19.7809 21.9181L18.8042 9.88248L16.4951 7.17289L9.23799 0H0.00172773Z'
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill='#161616'
|
||||||
|
opacity={0.06}
|
||||||
|
fillRule='evenodd'
|
||||||
|
clipRule='evenodd'
|
||||||
|
d='M7.69824 16.4364L12.5199 3.23696L16.5541 7.25596L7.69824 16.4364Z'
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill='#161616'
|
||||||
|
opacity={0.06}
|
||||||
|
fillRule='evenodd'
|
||||||
|
clipRule='evenodd'
|
||||||
|
d='M8.07751 15.9175L13.9419 4.63989L16.5849 7.28475L8.07751 15.9175Z'
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule='evenodd'
|
||||||
|
clipRule='evenodd'
|
||||||
|
fill={theme.palette.primary.main}
|
||||||
|
d='M7.77295 16.3566L23.6563 0H32V6.88383C32 6.88383 31.8262 9.17836 30.6591 10.4057L19.7824 22H13.6938L7.77295 16.3566Z'
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<CircularProgress disableShrink sx={{ mt: 6 }} />
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FallbackSpinner
|
||||||
35
src/@core/components/window-wrapper/index.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { useState, useEffect, ReactNode } from 'react'
|
||||||
|
|
||||||
|
// ** Next Import
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const WindowWrapper = ({ children }: Props) => {
|
||||||
|
// ** State
|
||||||
|
const [windowReadyFlag, setWindowReadyFlag] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
setWindowReadyFlag(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[router.route]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (windowReadyFlag) {
|
||||||
|
return <>{children}</>
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WindowWrapper
|
||||||
155
src/@core/context/settingsContext.tsx
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { createContext, useState, ReactNode, useEffect } from 'react'
|
||||||
|
|
||||||
|
// ** MUI Imports
|
||||||
|
import { Direction } from '@mui/material'
|
||||||
|
|
||||||
|
// ** ThemeConfig Import
|
||||||
|
import themeConfig from '../../configs/themeConfig'
|
||||||
|
|
||||||
|
// ** Types Import
|
||||||
|
import { Skin, Mode, AppBar, Footer, ThemeColor, ContentWidth, VerticalNavToggle } from '../../@core/layouts/types'
|
||||||
|
|
||||||
|
export type Settings = {
|
||||||
|
skin: Skin
|
||||||
|
mode: Mode
|
||||||
|
appBar?: AppBar
|
||||||
|
footer?: Footer
|
||||||
|
navHidden?: boolean // navigation menu
|
||||||
|
appBarBlur: boolean
|
||||||
|
direction: Direction
|
||||||
|
navCollapsed: boolean
|
||||||
|
themeColor: ThemeColor
|
||||||
|
contentWidth: ContentWidth
|
||||||
|
layout?: 'vertical' | 'horizontal'
|
||||||
|
lastLayout?: 'vertical' | 'horizontal'
|
||||||
|
verticalNavToggleType: VerticalNavToggle
|
||||||
|
toastPosition?: 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PageSpecificSettings = {
|
||||||
|
skin?: Skin
|
||||||
|
mode?: Mode
|
||||||
|
appBar?: AppBar
|
||||||
|
footer?: Footer
|
||||||
|
navHidden?: boolean // navigation menu
|
||||||
|
appBarBlur?: boolean
|
||||||
|
direction?: Direction
|
||||||
|
navCollapsed?: boolean
|
||||||
|
themeColor?: ThemeColor
|
||||||
|
contentWidth?: ContentWidth
|
||||||
|
layout?: 'vertical' | 'horizontal'
|
||||||
|
lastLayout?: 'vertical' | 'horizontal'
|
||||||
|
verticalNavToggleType?: VerticalNavToggle
|
||||||
|
toastPosition?: 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
|
||||||
|
}
|
||||||
|
export type SettingsContextValue = {
|
||||||
|
settings: Settings
|
||||||
|
saveSettings: (updatedSettings: Settings) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsProviderProps {
|
||||||
|
children: ReactNode
|
||||||
|
pageSettings?: PageSpecificSettings | void
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialSettings: Settings = {
|
||||||
|
themeColor: 'primary',
|
||||||
|
mode: themeConfig.mode,
|
||||||
|
skin: themeConfig.skin,
|
||||||
|
footer: themeConfig.footer,
|
||||||
|
layout: themeConfig.layout,
|
||||||
|
lastLayout: themeConfig.layout,
|
||||||
|
direction: themeConfig.direction,
|
||||||
|
navHidden: themeConfig.navHidden,
|
||||||
|
appBarBlur: themeConfig.appBarBlur,
|
||||||
|
navCollapsed: themeConfig.navCollapsed,
|
||||||
|
contentWidth: themeConfig.contentWidth,
|
||||||
|
toastPosition: themeConfig.toastPosition,
|
||||||
|
verticalNavToggleType: themeConfig.verticalNavToggleType,
|
||||||
|
appBar: themeConfig.layout === 'horizontal' && themeConfig.appBar === 'hidden' ? 'fixed' : themeConfig.appBar
|
||||||
|
}
|
||||||
|
|
||||||
|
const staticSettings = {
|
||||||
|
appBar: initialSettings.appBar,
|
||||||
|
footer: initialSettings.footer,
|
||||||
|
layout: initialSettings.layout,
|
||||||
|
navHidden: initialSettings.navHidden,
|
||||||
|
lastLayout: initialSettings.lastLayout,
|
||||||
|
toastPosition: initialSettings.toastPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreSettings = (): Settings | null => {
|
||||||
|
let settings = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const storedData: string | null = window.localStorage.getItem('settings')
|
||||||
|
|
||||||
|
if (storedData) {
|
||||||
|
settings = { ...JSON.parse(storedData), ...staticSettings }
|
||||||
|
} else {
|
||||||
|
settings = initialSettings
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings
|
||||||
|
}
|
||||||
|
|
||||||
|
// set settings in localStorage
|
||||||
|
const storeSettings = (settings: Settings) => {
|
||||||
|
const initSettings = Object.assign({}, settings)
|
||||||
|
|
||||||
|
delete initSettings.appBar
|
||||||
|
delete initSettings.footer
|
||||||
|
delete initSettings.layout
|
||||||
|
delete initSettings.navHidden
|
||||||
|
delete initSettings.lastLayout
|
||||||
|
delete initSettings.toastPosition
|
||||||
|
window.localStorage.setItem('settings', JSON.stringify(initSettings))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ** Create Context
|
||||||
|
export const SettingsContext = createContext<SettingsContextValue>({
|
||||||
|
saveSettings: () => null,
|
||||||
|
settings: initialSettings
|
||||||
|
})
|
||||||
|
|
||||||
|
export const SettingsProvider = ({ children, pageSettings }: SettingsProviderProps) => {
|
||||||
|
// ** State
|
||||||
|
const [settings, setSettings] = useState<Settings>({ ...initialSettings })
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const restoredSettings = restoreSettings()
|
||||||
|
|
||||||
|
if (restoredSettings) {
|
||||||
|
setSettings({ ...restoredSettings })
|
||||||
|
}
|
||||||
|
if (pageSettings) {
|
||||||
|
setSettings({ ...settings, ...pageSettings })
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [pageSettings])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (settings.layout === 'horizontal' && settings.mode === 'semi-dark') {
|
||||||
|
saveSettings({ ...settings, mode: 'light' })
|
||||||
|
}
|
||||||
|
if (settings.layout === 'horizontal' && settings.appBar === 'hidden') {
|
||||||
|
saveSettings({ ...settings, appBar: 'fixed' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [settings.layout])
|
||||||
|
|
||||||
|
const saveSettings = (updatedSettings: Settings) => {
|
||||||
|
storeSettings(updatedSettings)
|
||||||
|
setSettings(updatedSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SettingsContext.Provider value={{ settings, saveSettings }}>{children}</SettingsContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SettingsConsumer = SettingsContext.Consumer
|
||||||
70
src/@core/hooks/useBgColor.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import { useTheme } from '@mui/material/styles'
|
||||||
|
|
||||||
|
// ** Util Import
|
||||||
|
import { hexToRGBA } from '../utils/hex-to-rgba'
|
||||||
|
|
||||||
|
export type UseBgColorType = {
|
||||||
|
[key: string]: {
|
||||||
|
color: string
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const UseBgColor = () => {
|
||||||
|
// ** Hooks
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
return {
|
||||||
|
primaryFilled: {
|
||||||
|
color: theme.palette.primary.contrastText,
|
||||||
|
backgroundColor: theme.palette.primary.main
|
||||||
|
},
|
||||||
|
primaryLight: {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
backgroundColor: hexToRGBA(theme.palette.primary.main, 0.16)
|
||||||
|
},
|
||||||
|
secondaryFilled: {
|
||||||
|
color: theme.palette.secondary.contrastText,
|
||||||
|
backgroundColor: theme.palette.secondary.main
|
||||||
|
},
|
||||||
|
secondaryLight: {
|
||||||
|
color: theme.palette.secondary.main,
|
||||||
|
backgroundColor: hexToRGBA(theme.palette.secondary.main, 0.16)
|
||||||
|
},
|
||||||
|
successFilled: {
|
||||||
|
color: theme.palette.success.contrastText,
|
||||||
|
backgroundColor: theme.palette.success.main
|
||||||
|
},
|
||||||
|
successLight: {
|
||||||
|
color: theme.palette.success.main,
|
||||||
|
backgroundColor: hexToRGBA(theme.palette.success.main, 0.16)
|
||||||
|
},
|
||||||
|
errorFilled: {
|
||||||
|
color: theme.palette.error.contrastText,
|
||||||
|
backgroundColor: theme.palette.error.main
|
||||||
|
},
|
||||||
|
errorLight: {
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
backgroundColor: hexToRGBA(theme.palette.error.main, 0.16)
|
||||||
|
},
|
||||||
|
warningFilled: {
|
||||||
|
color: theme.palette.warning.contrastText,
|
||||||
|
backgroundColor: theme.palette.warning.main
|
||||||
|
},
|
||||||
|
warningLight: {
|
||||||
|
color: theme.palette.warning.main,
|
||||||
|
backgroundColor: hexToRGBA(theme.palette.warning.main, 0.16)
|
||||||
|
},
|
||||||
|
infoFilled: {
|
||||||
|
color: theme.palette.info.contrastText,
|
||||||
|
backgroundColor: theme.palette.info.main
|
||||||
|
},
|
||||||
|
infoLight: {
|
||||||
|
color: theme.palette.info.main,
|
||||||
|
backgroundColor: hexToRGBA(theme.palette.info.main, 0.16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UseBgColor
|
||||||
65
src/@core/hooks/useClipboard.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// ** React Imports
|
||||||
|
import { RefObject, useCallback, useRef } from 'react'
|
||||||
|
|
||||||
|
// ** Third Party Imports
|
||||||
|
import copy from 'clipboard-copy'
|
||||||
|
|
||||||
|
interface UseClipboardOptions {
|
||||||
|
copiedTimeout?: number
|
||||||
|
onSuccess?: () => void
|
||||||
|
onError?: () => void
|
||||||
|
selectOnCopy?: boolean
|
||||||
|
selectOnError?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClipboardAPI {
|
||||||
|
readonly copy: (text?: string | any) => void
|
||||||
|
readonly target: RefObject<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
const isInputLike = (node: any): node is HTMLInputElement | HTMLTextAreaElement => {
|
||||||
|
return node && (node.nodeName === 'TEXTAREA' || node.nodeName === 'INPUT')
|
||||||
|
}
|
||||||
|
|
||||||
|
const useClipboard = (options: UseClipboardOptions = {}): ClipboardAPI => {
|
||||||
|
const targetRef = useRef<HTMLTextAreaElement | HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const handleSuccess = () => {
|
||||||
|
if (options.onSuccess) {
|
||||||
|
options.onSuccess()
|
||||||
|
}
|
||||||
|
if (options.selectOnCopy && isInputLike(targetRef.current)) {
|
||||||
|
targetRef.current.select()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleError = () => {
|
||||||
|
if (options.onError) {
|
||||||
|
options.onError()
|
||||||
|
}
|
||||||
|
const selectOnError = options.selectOnError !== false
|
||||||
|
if (selectOnError && isInputLike(targetRef.current)) {
|
||||||
|
targetRef.current.select()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clipboardCopy = (text: string) => {
|
||||||
|
copy(text).then(handleSuccess).catch(handleError)
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyHandler = useCallback((text?: string | HTMLElement) => {
|
||||||
|
if (typeof text === 'string') {
|
||||||
|
clipboardCopy(text)
|
||||||
|
} else if (targetRef.current) {
|
||||||
|
clipboardCopy(targetRef.current.value)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
copy: copyHandler,
|
||||||
|
target: targetRef
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useClipboard
|
||||||
4
src/@core/hooks/useSettings.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { useContext } from 'react'
|
||||||
|
import { SettingsContext, SettingsContextValue } from '../context/settingsContext'
|
||||||
|
|
||||||
|
export const useSettings = (): SettingsContextValue => useContext(SettingsContext)
|
||||||
40
src/@core/layouts/BlankLayout.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import { styled } from '@mui/material/styles'
|
||||||
|
import Box, { BoxProps } from '@mui/material/Box'
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { BlankLayoutProps } from './types'
|
||||||
|
|
||||||
|
// Styled component for Blank Layout component
|
||||||
|
const BlankLayoutWrapper = styled(Box)<BoxProps>(({ theme }) => ({
|
||||||
|
height: '100vh',
|
||||||
|
|
||||||
|
// For V1 Blank layout pages
|
||||||
|
'& .content-center': {
|
||||||
|
display: 'flex',
|
||||||
|
minHeight: '100vh',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: theme.spacing(5)
|
||||||
|
},
|
||||||
|
|
||||||
|
// For V2 Blank layout pages
|
||||||
|
'& .content-right': {
|
||||||
|
display: 'flex',
|
||||||
|
minHeight: '100vh',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
position: 'relative'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const BlankLayout = ({ children }: BlankLayoutProps) => {
|
||||||
|
return (
|
||||||
|
<BlankLayoutWrapper className='layout-wrapper'>
|
||||||
|
<Box className='app-content' sx={{ overflow: 'hidden', minHeight: '100vh', position: 'relative' }}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</BlankLayoutWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlankLayout
|
||||||
54
src/@core/layouts/BlankLayoutWithAppBar.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import { styled } from '@mui/material/styles'
|
||||||
|
import Box, { BoxProps } from '@mui/material/Box'
|
||||||
|
|
||||||
|
// ** Types
|
||||||
|
import { BlankLayoutWithAppBarProps } from './types'
|
||||||
|
|
||||||
|
// ** AppBar Imports
|
||||||
|
import AppBar from 'src/@core/layouts/components/blank-layout-with-appBar'
|
||||||
|
|
||||||
|
// Styled component for Blank Layout with AppBar component
|
||||||
|
const BlankLayoutWithAppBarWrapper = styled(Box)<BoxProps>(({ theme }) => ({
|
||||||
|
height: '100vh',
|
||||||
|
|
||||||
|
// For V1 Blank layout pages
|
||||||
|
'& .content-center': {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: theme.spacing(5),
|
||||||
|
minHeight: `calc(100vh - ${theme.spacing((theme.mixins.toolbar.minHeight as number) / 4)})`
|
||||||
|
},
|
||||||
|
|
||||||
|
// For V2 Blank layout pages
|
||||||
|
'& .content-right': {
|
||||||
|
display: 'flex',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
minHeight: `calc(100vh - ${theme.spacing((theme.mixins.toolbar.minHeight as number) / 4)})`
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const BlankLayoutWithAppBar = (props: BlankLayoutWithAppBarProps) => {
|
||||||
|
// ** Props
|
||||||
|
const { children } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BlankLayoutWithAppBarWrapper>
|
||||||
|
<AppBar />
|
||||||
|
<Box
|
||||||
|
className='app-content'
|
||||||
|
sx={{
|
||||||
|
overflowX: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
minHeight: theme => `calc(100vh - ${theme.spacing((theme.mixins.toolbar.minHeight as number) / 4)})`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</BlankLayoutWithAppBarWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlankLayoutWithAppBar
|
||||||
195
src/@core/layouts/HorizontalLayout.tsx
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
// ** MUI Imports
|
||||||
|
import Fab from '@mui/material/Fab'
|
||||||
|
import AppBar from '@mui/material/AppBar'
|
||||||
|
import { styled } from '@mui/material/styles'
|
||||||
|
import Box, { BoxProps } from '@mui/material/Box'
|
||||||
|
import MuiToolbar, { ToolbarProps } from '@mui/material/Toolbar'
|
||||||
|
|
||||||
|
// ** Icon Imports
|
||||||
|
import Icon from 'src/@core/components/icon'
|
||||||
|
|
||||||
|
// ** Theme Config Import
|
||||||
|
import themeConfig from 'src/configs/themeConfig'
|
||||||
|
|
||||||
|
// ** Type Import
|
||||||
|
import { LayoutProps } from 'src/@core/layouts/types'
|
||||||
|
|
||||||
|
// ** Components
|
||||||
|
import Customizer from 'src/@core/components/customizer'
|
||||||
|
import Footer from './components/shared-components/footer'
|
||||||
|
import Navigation from './components/horizontal/navigation'
|
||||||
|
import ScrollToTop from 'src/@core/components/scroll-to-top'
|
||||||
|
import AppBarContent from './components/horizontal/app-bar-content'
|
||||||
|
|
||||||
|
// ** Util Import
|
||||||
|
import { hexToRGBA } from '../utils/hex-to-rgba'
|
||||||
|
|
||||||
|
const HorizontalLayoutWrapper = styled('div')({
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
...(themeConfig.horizontalMenuAnimation && { overflow: 'clip' })
|
||||||
|
})
|
||||||
|
|
||||||
|
const MainContentWrapper = styled(Box)<BoxProps>({
|
||||||
|
flexGrow: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
display: 'flex',
|
||||||
|
minHeight: '100vh',
|
||||||
|
flexDirection: 'column'
|
||||||
|
})
|
||||||
|
|
||||||
|
const Toolbar = styled(MuiToolbar)<ToolbarProps>(({ theme }) => ({
|
||||||
|
width: '100%',
|
||||||
|
padding: `${theme.spacing(0, 6)} !important`,
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
paddingLeft: theme.spacing(2),
|
||||||
|
paddingRight: theme.spacing(4)
|
||||||
|
},
|
||||||
|
[theme.breakpoints.down('xs')]: {
|
||||||
|
paddingLeft: theme.spacing(2),
|
||||||
|
paddingRight: theme.spacing(2)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const ContentWrapper = styled('main')(({ theme }) => ({
|
||||||
|
flexGrow: 1,
|
||||||
|
width: '100%',
|
||||||
|
padding: theme.spacing(6),
|
||||||
|
transition: 'padding .25s ease-in-out',
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
paddingLeft: theme.spacing(4),
|
||||||
|
paddingRight: theme.spacing(4)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const HorizontalLayout = (props: LayoutProps) => {
|
||||||
|
// ** Props
|
||||||
|
const {
|
||||||
|
hidden,
|
||||||
|
children,
|
||||||
|
settings,
|
||||||
|
scrollToTop,
|
||||||
|
footerProps,
|
||||||
|
saveSettings,
|
||||||
|
contentHeightFixed,
|
||||||
|
horizontalLayoutProps
|
||||||
|
} = props
|
||||||
|
|
||||||
|
// ** Vars
|
||||||
|
const { skin, appBar, navHidden, appBarBlur, contentWidth } = settings
|
||||||
|
const appBarProps = horizontalLayoutProps?.appBar?.componentProps
|
||||||
|
const userNavMenuContent = horizontalLayoutProps?.navMenu?.content
|
||||||
|
|
||||||
|
let userAppBarStyle = {}
|
||||||
|
if (appBarProps && appBarProps.sx) {
|
||||||
|
userAppBarStyle = appBarProps.sx
|
||||||
|
}
|
||||||
|
const userAppBarProps = Object.assign({}, appBarProps)
|
||||||
|
delete userAppBarProps.sx
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HorizontalLayoutWrapper className='layout-wrapper'>
|
||||||
|
<MainContentWrapper className='layout-content-wrapper' sx={{ ...(contentHeightFixed && { maxHeight: '100vh' }) }}>
|
||||||
|
{/* Navbar (or AppBar) and Navigation Menu Wrapper */}
|
||||||
|
<AppBar
|
||||||
|
color='default'
|
||||||
|
elevation={skin === 'bordered' ? 0 : 4}
|
||||||
|
className='layout-navbar-and-nav-container'
|
||||||
|
position={appBar === 'fixed' ? 'sticky' : 'static'}
|
||||||
|
sx={{
|
||||||
|
alignItems: 'center',
|
||||||
|
color: 'text.primary',
|
||||||
|
justifyContent: 'center',
|
||||||
|
...(appBar === 'static' && { zIndex: 13 }),
|
||||||
|
transition: 'border-bottom 0.2s ease-in-out',
|
||||||
|
...(appBarBlur && { backdropFilter: 'blur(6px)' }),
|
||||||
|
backgroundColor: theme => hexToRGBA(theme.palette.background.paper, appBarBlur ? 0.95 : 1),
|
||||||
|
...(skin === 'bordered' && { borderBottom: theme => `1px solid ${theme.palette.divider}` }),
|
||||||
|
...userAppBarStyle
|
||||||
|
}}
|
||||||
|
{...userAppBarProps}
|
||||||
|
>
|
||||||
|
{/* Navbar / AppBar */}
|
||||||
|
<Box
|
||||||
|
className='layout-navbar'
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
...(navHidden ? {} : { borderBottom: theme => `1px solid ${theme.palette.divider}` })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Toolbar
|
||||||
|
className='navbar-content-container'
|
||||||
|
sx={{
|
||||||
|
mx: 'auto',
|
||||||
|
...(contentWidth === 'boxed' && { '@media (min-width:1440px)': { maxWidth: 1440 } }),
|
||||||
|
minHeight: theme => `${(theme.mixins.toolbar.minHeight as number) - 1}px !important`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AppBarContent
|
||||||
|
{...props}
|
||||||
|
hidden={hidden}
|
||||||
|
settings={settings}
|
||||||
|
saveSettings={saveSettings}
|
||||||
|
appBarContent={horizontalLayoutProps?.appBar?.content}
|
||||||
|
appBarBranding={horizontalLayoutProps?.appBar?.branding}
|
||||||
|
/>
|
||||||
|
</Toolbar>
|
||||||
|
</Box>
|
||||||
|
{/* Navigation Menu */}
|
||||||
|
{navHidden ? null : (
|
||||||
|
<Box className='layout-horizontal-nav' sx={{ width: '100%', ...horizontalLayoutProps?.navMenu?.sx }}>
|
||||||
|
<Toolbar
|
||||||
|
className='horizontal-nav-content-container'
|
||||||
|
sx={{
|
||||||
|
mx: 'auto',
|
||||||
|
...(contentWidth === 'boxed' && { '@media (min-width:1440px)': { maxWidth: 1440 } }),
|
||||||
|
minHeight: theme =>
|
||||||
|
`${(theme.mixins.toolbar.minHeight as number) - 4 - (skin === 'bordered' ? 1 : 0)}px !important`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(userNavMenuContent && userNavMenuContent(props)) || (
|
||||||
|
<Navigation
|
||||||
|
{...props}
|
||||||
|
horizontalNavItems={
|
||||||
|
(horizontalLayoutProps as NonNullable<LayoutProps['horizontalLayoutProps']>).navMenu?.navItems
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Toolbar>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</AppBar>
|
||||||
|
{/* Content */}
|
||||||
|
<ContentWrapper
|
||||||
|
className='layout-page-content'
|
||||||
|
sx={{
|
||||||
|
...(contentHeightFixed && { display: 'flex', overflow: 'hidden' }),
|
||||||
|
...(contentWidth === 'boxed' && {
|
||||||
|
mx: 'auto',
|
||||||
|
'@media (min-width:1440px)': { maxWidth: 1440 },
|
||||||
|
'@media (min-width:1200px)': { maxWidth: '100%' }
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ContentWrapper>
|
||||||
|
{/* Footer */}
|
||||||
|
<Footer {...props} footerStyles={footerProps?.sx} footerContent={footerProps?.content} />
|
||||||
|
{/* Customizer */}
|
||||||
|
{themeConfig.disableCustomizer || hidden ? null : <Customizer />}
|
||||||
|
{/* Scroll to top button */}
|
||||||
|
{scrollToTop ? (
|
||||||
|
scrollToTop(props)
|
||||||
|
) : (
|
||||||
|
<ScrollToTop className='mui-fixed'>
|
||||||
|
<Fab color='primary' size='small' aria-label='scroll back to top'>
|
||||||
|
<Icon icon='tabler:arrow-up' />
|
||||||
|
</Fab>
|
||||||
|
</ScrollToTop>
|
||||||
|
)}
|
||||||
|
</MainContentWrapper>
|
||||||
|
</HorizontalLayoutWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HorizontalLayout
|
||||||
45
src/@core/layouts/Layout.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// ** React Import
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
// ** Type Import
|
||||||
|
import { LayoutProps } from 'src/@core/layouts/types'
|
||||||
|
|
||||||
|
// ** Layout Components
|
||||||
|
import VerticalLayout from './VerticalLayout'
|
||||||
|
import HorizontalLayout from './HorizontalLayout'
|
||||||
|
|
||||||
|
const Layout = (props: LayoutProps) => {
|
||||||
|
// ** Props
|
||||||
|
const { hidden, children, settings, saveSettings } = props
|
||||||
|
|
||||||
|
// ** Ref
|
||||||
|
const isCollapsed = useRef(settings.navCollapsed)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hidden) {
|
||||||
|
if (settings.navCollapsed) {
|
||||||
|
saveSettings({ ...settings, navCollapsed: false, layout: 'vertical' })
|
||||||
|
isCollapsed.current = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isCollapsed.current) {
|
||||||
|
saveSettings({ ...settings, navCollapsed: true, layout: settings.lastLayout })
|
||||||
|
isCollapsed.current = false
|
||||||
|
} else {
|
||||||
|
if (settings.lastLayout !== settings.layout) {
|
||||||
|
saveSettings({ ...settings, layout: settings.lastLayout })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [hidden])
|
||||||
|
|
||||||
|
if (settings.layout === 'horizontal') {
|
||||||
|
return <HorizontalLayout {...props}>{children}</HorizontalLayout>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <VerticalLayout {...props}>{children}</VerticalLayout>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Layout
|
||||||