first commit

This commit is contained in:
rizal.productzilla 2024-04-16 16:48:48 +07:00
parent 11959746d0
commit 0e91646c75
209 changed files with 28786 additions and 22 deletions

64
app/401.tsx Normal file
View 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
View 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
View 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
View 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
View 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
View 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

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

View 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

View 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

View 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

View File

@ -0,0 +1,15 @@
interface Props {
text: string
}
const Translations = ({ text }: Props) => {
return <>{text}</>
}
export default Translations

View 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

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

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

@ -0,0 +1,11 @@
module.exports = {
async redirects() {
return [
{
source: '/',
destination: '/login',
permanent: true,
},
]
},
}

View File

@ -1,4 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
public/images/avatars/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
public/images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
public/images/pages/401.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
public/images/pages/404.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

View 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

View 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

View 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

View 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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

View 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

View 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

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

View 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

View 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

View 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

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

View 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

View 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

View 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

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

View 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

View File

@ -0,0 +1,4 @@
// ** MUI Imports
import { BadgeProps } from '@mui/material/Badge'
export type CustomBadgeProps = BadgeProps & { skin?: 'light' }

View 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

View File

@ -0,0 +1,4 @@
// ** MUI Imports
import { ChipProps } from '@mui/material/Chip'
export type CustomChipProps = ChipProps & { skin?: 'light'; rounded?: boolean }

View 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

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

View 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

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

View 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

View File

@ -0,0 +1,6 @@
import { ReactNode } from 'react'
export type PageHeaderProps = {
title: ReactNode
subtitle?: ReactNode
}

View 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

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

View 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

View 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

View 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

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

View 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

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

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

View 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

View 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

View 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

View 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

View 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

View File

@ -0,0 +1,4 @@
import { useContext } from 'react'
import { SettingsContext, SettingsContextValue } from '../context/settingsContext'
export const useSettings = (): SettingsContextValue => useContext(SettingsContext)

View 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

View 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

View 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

View 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

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