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"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"next": "14.2.1"
|
||||
"@casl/ability": "6.3.3",
|
||||
"@casl/react": "3.1.0",
|
||||
"@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": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.1"
|
||||
"@iconify/iconify": "3.0.1",
|
||||
"@iconify/json": "2.2.4",
|
||||
"@iconify/tools": "2.2.0",
|
||||
"@iconify/types": "2.0.0",
|
||||
"@iconify/utils": "2.0.11",
|
||||
"@types/cleave.js": "1.4.7",
|
||||
"@types/draft-js": "0.11.10",
|
||||
"@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
|
||||