init: add fuse-react v8.3.5 skeleton

This commit is contained in:
2023-05-30 21:07:44 +01:00
commit 3a760d2646
543 changed files with 102541 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
import FuseDialog from '@fuse/core/FuseDialog';
import { styled } from '@mui/material/styles';
import FuseMessage from '@fuse/core/FuseMessage';
import FuseSuspense from '@fuse/core/FuseSuspense';
import AppContext from 'app/AppContext';
import { memo, useContext } from 'react';
import { useSelector } from 'react-redux';
import { useRoutes } from 'react-router-dom';
import { selectFuseCurrentLayoutConfig } from 'app/store/fuse/settingsSlice';
import FooterLayout1 from './components/FooterLayout1';
import LeftSideLayout1 from './components/LeftSideLayout1';
import NavbarWrapperLayout1 from './components/NavbarWrapperLayout1';
import RightSideLayout1 from './components/RightSideLayout1';
import ToolbarLayout1 from './components/ToolbarLayout1';
import SettingsPanel from '../shared-components/SettingsPanel';
const Root = styled('div')(({ theme, config }) => ({
...(config.mode === 'boxed' && {
clipPath: 'inset(0)',
maxWidth: `${config.containerWidth}px`,
margin: '0 auto',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
}),
...(config.mode === 'container' && {
'& .container': {
maxWidth: `${config.containerWidth}px`,
width: '100%',
margin: '0 auto',
},
}),
}));
function Layout1(props) {
const config = useSelector(selectFuseCurrentLayoutConfig);
const appContext = useContext(AppContext);
const { routes } = appContext;
return (
<Root id="fuse-layout" config={config} className="w-full flex">
{config.leftSidePanel.display && <LeftSideLayout1 />}
<div className="flex flex-auto min-w-0">
{config.navbar.display && config.navbar.position === 'left' && <NavbarWrapperLayout1 />}
<main id="fuse-main" className="flex flex-col flex-auto min-h-full min-w-0 relative z-10">
{config.toolbar.display && (
<ToolbarLayout1 className={config.toolbar.style === 'fixed' && 'sticky top-0'} />
)}
<div className="sticky top-0 z-99">
<SettingsPanel />
</div>
<div className="flex flex-col flex-auto min-h-0 relative z-10">
<FuseDialog />
<FuseSuspense>{useRoutes(routes)}</FuseSuspense>
{props.children}
</div>
{config.footer.display && (
<FooterLayout1 className={config.footer.style === 'fixed' && 'sticky bottom-0'} />
)}
</main>
{config.navbar.display && config.navbar.position === 'right' && <NavbarWrapperLayout1 />}
</div>
{config.rightSidePanel.display && <RightSideLayout1 />}
<FuseMessage />
</Root>
);
}
export default memo(Layout1);

View File

@@ -0,0 +1,152 @@
const config = {
title: 'Layout 1 - Vertical',
defaults: {
mode: 'container',
containerWidth: 1570,
navbar: {
display: true,
style: 'style-1',
folded: true,
position: 'left',
},
toolbar: {
display: true,
style: 'fixed',
},
footer: {
display: true,
style: 'fixed',
},
leftSidePanel: {
display: true,
},
rightSidePanel: {
display: true,
},
},
form: {
mode: {
title: 'Mode',
type: 'radio',
options: [
{
name: 'Boxed',
value: 'boxed',
},
{
name: 'Full Width',
value: 'fullwidth',
},
{
name: 'Container',
value: 'container',
},
],
},
containerWidth: {
title: 'Container Width (px)',
type: 'number',
},
navbar: {
type: 'group',
title: 'Navbar',
children: {
display: {
title: 'Display',
type: 'switch',
},
position: {
title: 'Position',
type: 'radio',
options: [
{
name: 'Left',
value: 'left',
},
{
name: 'Right',
value: 'right',
},
],
},
style: {
title: 'Style',
type: 'radio',
options: [
{
name: 'Slide (style-1)',
value: 'style-1',
},
{
name: 'Folded (style-2)',
value: 'style-2',
},
{
name: 'Tabbed (style-3)',
value: 'style-3',
},
{
name: 'Tabbed Dense (style-3-dense)',
value: 'style-3-dense',
},
],
},
folded: {
title: 'Folded (style-2, style-3)',
type: 'switch',
},
},
},
toolbar: {
type: 'group',
title: 'Toolbar',
children: {
display: {
title: 'Display',
type: 'switch',
},
style: {
title: 'Style',
type: 'radio',
options: [
{
name: 'Fixed',
value: 'fixed',
},
{
name: 'Static',
value: 'static',
},
],
},
},
},
footer: {
type: 'group',
title: 'Footer',
children: {
display: {
title: 'Display',
type: 'switch',
},
style: {
title: 'Style',
type: 'radio',
options: [
{
name: 'Fixed',
value: 'fixed',
},
{
name: 'Static',
value: 'static',
},
],
},
},
},
},
};
export default config;

View File

@@ -0,0 +1,33 @@
import AppBar from '@mui/material/AppBar';
import { ThemeProvider } from '@mui/material/styles';
import Toolbar from '@mui/material/Toolbar';
import { memo } from 'react';
import { useSelector } from 'react-redux';
import { selectFooterTheme } from 'app/store/fuse/settingsSlice';
import clsx from 'clsx';
function FooterLayout1(props) {
const footerTheme = useSelector(selectFooterTheme);
return (
<ThemeProvider theme={footerTheme}>
<AppBar
id="fuse-footer"
className={clsx('relative z-20 shadow-md', props.className)}
color="default"
sx={{
backgroundColor: (theme) =>
theme.palette.mode === 'light'
? footerTheme.palette.background.paper
: footerTheme.palette.background.default,
}}
>
<Toolbar className="min-h-48 md:min-h-64 px-8 sm:px-12 py-0 flex items-center overflow-x-auto">
Footer
</Toolbar>
</AppBar>
</ThemeProvider>
);
}
export default memo(FooterLayout1);

View File

@@ -0,0 +1,7 @@
import { memo } from 'react';
function LeftSideLayout1() {
return <></>;
}
export default memo(LeftSideLayout1);

View File

@@ -0,0 +1,33 @@
import { ThemeProvider } from '@mui/material/styles';
import { memo } from 'react';
import { useSelector } from 'react-redux';
import { selectFuseCurrentLayoutConfig, selectNavbarTheme } from 'app/store/fuse/settingsSlice';
import { selectFuseNavbar } from 'app/store/fuse/navbarSlice';
import NavbarStyle1 from './navbar/style-1/NavbarStyle1';
import NavbarStyle2 from './navbar/style-2/NavbarStyle2';
import NavbarStyle3 from './navbar/style-3/NavbarStyle3';
import NavbarToggleFab from '../../shared-components/NavbarToggleFab';
function NavbarWrapperLayout1(props) {
const config = useSelector(selectFuseCurrentLayoutConfig);
const navbar = useSelector(selectFuseNavbar);
const navbarTheme = useSelector(selectNavbarTheme);
return (
<>
<ThemeProvider theme={navbarTheme}>
<>
{config.navbar.style === 'style-1' && <NavbarStyle1 />}
{config.navbar.style === 'style-2' && <NavbarStyle2 />}
{config.navbar.style === 'style-3' && <NavbarStyle3 />}
{config.navbar.style === 'style-3-dense' && <NavbarStyle3 dense />}
</>
</ThemeProvider>
{config.navbar.display && !config.toolbar.display && !navbar.open && <NavbarToggleFab />}
</>
);
}
export default memo(NavbarWrapperLayout1);

View File

@@ -0,0 +1,15 @@
import { memo } from 'react';
import QuickPanel from '../../shared-components/quickPanel/QuickPanel';
import NotificationPanel from '../../shared-components/notificationPanel/NotificationPanel';
function RightSideLayout1(props) {
return (
<>
<QuickPanel />
<NotificationPanel />
</>
);
}
export default memo(RightSideLayout1);

View File

@@ -0,0 +1,103 @@
import { ThemeProvider } from '@mui/material/styles';
import AppBar from '@mui/material/AppBar';
import Hidden from '@mui/material/Hidden';
import Toolbar from '@mui/material/Toolbar';
import clsx from 'clsx';
import { memo } from 'react';
import { useSelector } from 'react-redux';
import { selectFuseCurrentLayoutConfig, selectToolbarTheme } from 'app/store/fuse/settingsSlice';
import { selectFuseNavbar } from 'app/store/fuse/navbarSlice';
import AdjustFontSize from '../../shared-components/AdjustFontSize';
import FullScreenToggle from '../../shared-components/FullScreenToggle';
import LanguageSwitcher from '../../shared-components/LanguageSwitcher';
import NotificationPanelToggleButton from '../../shared-components/notificationPanel/NotificationPanelToggleButton';
import NavigationShortcuts from '../../shared-components/NavigationShortcuts';
import NavigationSearch from '../../shared-components/NavigationSearch';
import NavbarToggleButton from '../../shared-components/NavbarToggleButton';
import UserMenu from '../../shared-components/UserMenu';
import QuickPanelToggleButton from '../../shared-components/quickPanel/QuickPanelToggleButton';
import ChatPanelToggleButton from '../../shared-components/chatPanel/ChatPanelToggleButton';
function ToolbarLayout1(props) {
const config = useSelector(selectFuseCurrentLayoutConfig);
const navbar = useSelector(selectFuseNavbar);
const toolbarTheme = useSelector(selectToolbarTheme);
return (
<ThemeProvider theme={toolbarTheme}>
<AppBar
id="fuse-toolbar"
className={clsx('flex relative z-20 shadow-md', props.className)}
color="default"
sx={{
backgroundColor: (theme) =>
theme.palette.mode === 'light'
? toolbarTheme.palette.background.paper
: toolbarTheme.palette.background.default,
}}
position="static"
>
<Toolbar className="p-0 min-h-48 md:min-h-64">
<div className="flex flex-1 px-16">
{config.navbar.display && config.navbar.position === 'left' && (
<>
<Hidden lgDown>
{(config.navbar.style === 'style-3' ||
config.navbar.style === 'style-3-dense') && (
<NavbarToggleButton className="w-40 h-40 p-0 mx-0" />
)}
{config.navbar.style === 'style-1' && !navbar.open && (
<NavbarToggleButton className="w-40 h-40 p-0 mx-0" />
)}
</Hidden>
<Hidden lgUp>
<NavbarToggleButton className="w-40 h-40 p-0 mx-0 sm:mx-8" />
</Hidden>
</>
)}
<Hidden lgDown>
<NavigationShortcuts />
</Hidden>
</div>
<div className="flex items-center px-8 h-full overflow-x-auto">
<LanguageSwitcher />
<AdjustFontSize />
<FullScreenToggle />
<NavigationSearch />
<Hidden lgUp>
<ChatPanelToggleButton />
</Hidden>
<QuickPanelToggleButton />
<NotificationPanelToggleButton />
<UserMenu />
</div>
{config.navbar.display && config.navbar.position === 'right' && (
<>
<Hidden lgDown>
{!navbar.open && <NavbarToggleButton className="w-40 h-40 p-0 mx-0" />}
</Hidden>
<Hidden lgUp>
<NavbarToggleButton className="w-40 h-40 p-0 mx-0 sm:mx-8" />
</Hidden>
</>
)}
</Toolbar>
</AppBar>
</ThemeProvider>
);
}
export default memo(ToolbarLayout1);

View File

@@ -0,0 +1,82 @@
import Hidden from '@mui/material/Hidden';
import { styled } from '@mui/material/styles';
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
import { useDispatch, useSelector } from 'react-redux';
import { navbarCloseMobile, selectFuseNavbar } from 'app/store/fuse/navbarSlice';
import { selectFuseCurrentLayoutConfig } from 'app/store/fuse/settingsSlice';
import NavbarStyle1Content from './NavbarStyle1Content';
const navbarWidth = 280;
const StyledNavBar = styled('div')(({ theme, open, position }) => ({
minWidth: navbarWidth,
width: navbarWidth,
maxWidth: navbarWidth,
...(!open && {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.leavingScreen,
}),
...(position === 'left' && {
marginLeft: `-${navbarWidth}px`,
}),
...(position === 'right' && {
marginRight: `-${navbarWidth}px`,
}),
}),
...(open && {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
}),
}));
const StyledNavBarMobile = styled(SwipeableDrawer)(({ theme }) => ({
'& .MuiDrawer-paper': {
minWidth: navbarWidth,
width: navbarWidth,
maxWidth: navbarWidth,
},
}));
function NavbarStyle1(props) {
const dispatch = useDispatch();
const config = useSelector(selectFuseCurrentLayoutConfig);
const navbar = useSelector(selectFuseNavbar);
return (
<>
<Hidden lgDown>
<StyledNavBar
className="flex-col flex-auto sticky top-0 overflow-hidden h-screen shrink-0 z-20 shadow-5"
open={navbar.open}
position={config.navbar.position}
>
<NavbarStyle1Content />
</StyledNavBar>
</Hidden>
<Hidden lgUp>
<StyledNavBarMobile
classes={{
paper: 'flex-col flex-auto h-full',
}}
anchor={config.navbar.position}
variant="temporary"
open={navbar.mobileOpen}
onClose={() => dispatch(navbarCloseMobile())}
onOpen={() => {}}
disableSwipeToOpen
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
>
<NavbarStyle1Content />
</StyledNavBarMobile>
</Hidden>
</>
);
}
export default NavbarStyle1;

View File

@@ -0,0 +1,62 @@
import FuseScrollbars from '@fuse/core/FuseScrollbars';
import { styled } from '@mui/material/styles';
import clsx from 'clsx';
import { memo } from 'react';
import Logo from '../../../../shared-components/Logo';
import NavbarToggleButton from '../../../../shared-components/NavbarToggleButton';
import UserNavbarHeader from '../../../../shared-components/UserNavbarHeader';
import Navigation from '../../../../shared-components/Navigation';
const Root = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
'& ::-webkit-scrollbar-thumb': {
boxShadow: `inset 0 0 0 20px ${
theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.24)' : 'rgba(255, 255, 255, 0.24)'
}`,
},
'& ::-webkit-scrollbar-thumb:active': {
boxShadow: `inset 0 0 0 20px ${
theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.37)' : 'rgba(255, 255, 255, 0.37)'
}`,
},
}));
const StyledContent = styled(FuseScrollbars)(({ theme }) => ({
overscrollBehavior: 'contain',
overflowX: 'hidden',
overflowY: 'auto',
WebkitOverflowScrolling: 'touch',
backgroundRepeat: 'no-repeat',
backgroundSize: '100% 40px, 100% 10px',
backgroundAttachment: 'local, scroll',
}));
function NavbarStyle1Content(props) {
return (
<Root className={clsx('flex flex-auto flex-col overflow-hidden h-full', props.className)}>
<div className="flex flex-row items-center shrink-0 h-48 md:h-72 px-20">
<div className="flex flex-1 mx-4">
<Logo />
</div>
<NavbarToggleButton className="w-40 h-40 p-0" />
</div>
<StyledContent
className="flex flex-1 flex-col min-h-0"
option={{ suppressScrollX: true, wheelPropagation: false }}
>
<UserNavbarHeader />
<Navigation layout="vertical" />
<div className="flex flex-0 items-center justify-center py-48 opacity-10">
<img className="w-full max-w-64" src="assets/images/logo/logo.svg" alt="footer logo" />
</div>
</StyledContent>
</Root>
);
}
export default memo(NavbarStyle1Content);

View File

@@ -0,0 +1,171 @@
import Hidden from '@mui/material/Hidden';
import { styled } from '@mui/material/styles';
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
import {
navbarCloseFolded,
navbarCloseMobile,
navbarOpenFolded,
selectFuseNavbar,
} from 'app/store/fuse/navbarSlice';
import { useDispatch, useSelector } from 'react-redux';
import { selectFuseCurrentLayoutConfig } from 'app/store/fuse/settingsSlice';
import NavbarStyle2Content from './NavbarStyle2Content';
const navbarWidth = 280;
const Root = styled('div')(({ theme, folded }) => ({
display: 'flex',
flexDirection: 'column',
zIndex: 4,
[theme.breakpoints.up('lg')]: {
width: navbarWidth,
minWidth: navbarWidth,
},
...(folded && {
[theme.breakpoints.up('lg')]: {
width: 76,
minWidth: 76,
},
}),
}));
const StyledNavbar = styled('div')(
({ theme, position, folded, foldedandopened, foldedandclosed }) => ({
minWidth: navbarWidth,
width: navbarWidth,
maxWidth: navbarWidth,
maxHeight: '100%',
transition: theme.transitions.create(['width', 'min-width'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.shorter,
}),
...(position === 'left' && {
left: 0,
}),
...(position === 'right' && {
right: 0,
}),
...(folded && {
position: 'absolute',
width: 76,
minWidth: 76,
top: 0,
bottom: 0,
}),
...(foldedandopened && {
width: navbarWidth,
minWidth: navbarWidth,
}),
...(foldedandclosed && {
'& .NavbarStyle2-content': {
'& .logo-icon': {
width: 44,
height: 44,
},
'& .logo-text': {
opacity: 0,
},
'& .react-badge': {
opacity: 0,
},
'& .fuse-list-item': {
width: 56,
},
'& .fuse-list-item-text, & .arrow-icon, & .item-badge': {
opacity: 0,
},
'& .fuse-list-subheader .fuse-list-subheader-text': {
opacity: 0,
},
'& .fuse-list-subheader:before': {
content: '""',
display: 'block',
position: 'absolute',
minWidth: 16,
borderTop: '2px solid',
opacity: 0.2,
},
'& .collapse-children': {
display: 'none',
},
},
}),
})
);
const StyledNavbarMobile = styled(SwipeableDrawer)(({ theme, position }) => ({
'& > .MuiDrawer-paper': {
minWidth: navbarWidth,
width: navbarWidth,
maxWidth: navbarWidth,
maxHeight: '100%',
transition: theme.transitions.create(['width', 'min-width'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.shorter,
}),
},
}));
function NavbarStyle2(props) {
const dispatch = useDispatch();
const config = useSelector(selectFuseCurrentLayoutConfig);
const navbar = useSelector(selectFuseNavbar);
// const folded = !navbar.open;
const { folded } = config.navbar;
const foldedandclosed = folded && !navbar.foldedOpen;
const foldedandopened = folded && navbar.foldedOpen;
return (
<Root
folded={folded ? 1 : 0}
open={navbar.open}
id="fuse-navbar"
className="sticky top-0 h-screen shrink-0 z-20 shadow-5"
>
<Hidden lgDown>
<StyledNavbar
className="flex-col flex-auto"
position={config.navbar.position}
folded={folded ? 1 : 0}
foldedandopened={foldedandopened ? 1 : 0}
foldedandclosed={foldedandclosed ? 1 : 0}
onMouseEnter={() => foldedandclosed && dispatch(navbarOpenFolded())}
onMouseLeave={() => foldedandopened && dispatch(navbarCloseFolded())}
>
<NavbarStyle2Content className="NavbarStyle2-content" />
</StyledNavbar>
</Hidden>
<Hidden lgUp>
<StyledNavbarMobile
classes={{
paper: 'flex-col flex-auto h-full',
}}
folded={folded ? 1 : 0}
foldedandopened={foldedandopened ? 1 : 0}
foldedandclosed={foldedandclosed ? 1 : 0}
anchor={config.navbar.position}
variant="temporary"
open={navbar.mobileOpen}
onClose={() => dispatch(navbarCloseMobile())}
onOpen={() => {}}
disableSwipeToOpen
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
>
<NavbarStyle2Content className="NavbarStyle2-content" />
</StyledNavbarMobile>
</Hidden>
</Root>
);
}
export default NavbarStyle2;

View File

@@ -0,0 +1,54 @@
import FuseScrollbars from '@fuse/core/FuseScrollbars';
import { styled } from '@mui/material/styles';
import clsx from 'clsx';
import { memo } from 'react';
import Logo from '../../../../shared-components/Logo';
import NavbarToggleButton from '../../../../shared-components/NavbarToggleButton';
import Navigation from '../../../../shared-components/Navigation';
const Root = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
'& ::-webkit-scrollbar-thumb': {
boxShadow: `inset 0 0 0 20px ${
theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.24)' : 'rgba(255, 255, 255, 0.24)'
}`,
},
'& ::-webkit-scrollbar-thumb:active': {
boxShadow: `inset 0 0 0 20px ${
theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.37)' : 'rgba(255, 255, 255, 0.37)'
}`,
},
}));
const StyledContent = styled(FuseScrollbars)(({ theme }) => ({
overscrollBehavior: 'contain',
overflowX: 'hidden',
overflowY: 'auto',
WebkitOverflowScrolling: 'touch',
background:
'linear-gradient(rgba(0, 0, 0, 0) 30%, rgba(0, 0, 0, 0) 30%), linear-gradient(rgba(0, 0, 0, 0.25) 0, rgba(0, 0, 0, 0) 40%)',
backgroundRepeat: 'no-repeat',
backgroundSize: '100% 40px, 100% 10px',
backgroundAttachment: 'local, scroll',
}));
function NavbarStyle2Content(props) {
return (
<Root className={clsx('flex flex-auto flex-col overflow-hidden h-full', props.className)}>
<div className="flex flex-row items-center shrink-0 h-48 md:h-76 px-12">
<div className="flex flex-1 mx-4">
<Logo />
</div>
<NavbarToggleButton className="w-40 h-40 p-0" />
</div>
<StyledContent option={{ suppressScrollX: true, wheelPropagation: false }}>
<Navigation layout="vertical" />
</StyledContent>
</Root>
);
}
export default memo(NavbarStyle2Content);

View File

@@ -0,0 +1,149 @@
import Hidden from '@mui/material/Hidden';
import { styled } from '@mui/material/styles';
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
import { useDispatch, useSelector } from 'react-redux';
import { navbarCloseMobile, selectFuseNavbar } from 'app/store/fuse/navbarSlice';
import GlobalStyles from '@mui/material/GlobalStyles';
import { selectFuseCurrentLayoutConfig } from 'app/store/fuse/settingsSlice';
import NavbarStyle3Content from './NavbarStyle3Content';
const navbarWidth = 120;
const navbarWidthDense = 64;
const panelWidth = 280;
const StyledNavBar = styled('div')(({ theme, dense, open, folded, position }) => ({
minWidth: navbarWidth,
width: navbarWidth,
maxWidth: navbarWidth,
...(dense && {
minWidth: navbarWidthDense,
width: navbarWidthDense,
maxWidth: navbarWidthDense,
...(!open && {
...(position === 'left' && {
marginLeft: -navbarWidthDense,
}),
...(position === 'right' && {
marginRight: -navbarWidthDense,
}),
}),
}),
...(!folded && {
minWidth: dense ? navbarWidthDense + panelWidth : navbarWidth + panelWidth,
width: dense ? navbarWidthDense + panelWidth : navbarWidth + panelWidth,
maxWidth: dense ? navbarWidthDense + panelWidth : navbarWidth + panelWidth,
'& #fuse-navbar-panel': {
opacity: '1!important',
pointerEvents: 'initial!important',
},
...(!open && {
...(position === 'left' && {
marginLeft: -(dense ? navbarWidthDense + panelWidth : navbarWidth + panelWidth),
}),
...(position === 'right' && {
marginRight: -(dense ? navbarWidthDense + panelWidth : navbarWidth + panelWidth),
}),
}),
}),
...(!open && {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.leavingScreen,
}),
...(position === 'left' && {
marginLeft: -(dense ? navbarWidthDense : navbarWidth),
}),
...(position === 'right' && {
marginRight: -(dense ? navbarWidthDense : navbarWidth),
}),
}),
...(open && {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
}),
}));
const StyledNavBarMobile = styled(SwipeableDrawer)(({ theme }) => ({
'& .MuiDrawer-paper': {
'& #fuse-navbar-side-panel': {
minWidth: 'auto',
wdith: 'auto',
},
'& #fuse-navbar-panel': {
opacity: '1!important',
pointerEvents: 'initial!important',
},
},
}));
function NavbarStyle3(props) {
const dispatch = useDispatch();
const config = useSelector(selectFuseCurrentLayoutConfig);
const navbar = useSelector(selectFuseNavbar);
const { folded } = config.navbar;
return (
<>
<GlobalStyles
styles={(theme) => ({
'& #fuse-navbar-side-panel': {
width: props.dense ? navbarWidthDense : navbarWidth,
minWidth: props.dense ? navbarWidthDense : navbarWidth,
maxWidth: props.dense ? navbarWidthDense : navbarWidth,
},
'& #fuse-navbar-panel': {
maxWidth: '100%',
width: panelWidth,
[theme.breakpoints.up('lg')]: {
minWidth: panelWidth,
maxWidth: 'initial',
},
},
})}
/>
<Hidden lgDown>
<StyledNavBar
open={navbar.open}
dense={props.dense ? 1 : 0}
folded={folded ? 1 : 0}
position={config.navbar.position}
className="flex-col flex-auto sticky top-0 h-screen shrink-0 z-20 shadow-5"
>
<NavbarStyle3Content dense={props.dense ? 1 : 0} folded={folded ? 1 : 0} />
</StyledNavBar>
</Hidden>
<Hidden lgUp>
<StyledNavBarMobile
classes={{
paper: 'flex-col flex-auto h-screen max-w-full w-auto overflow-hidden',
}}
anchor={config.navbar.position}
variant="temporary"
open={navbar.mobileOpen}
onClose={() => dispatch(navbarCloseMobile())}
onOpen={() => {}}
disableSwipeToOpen
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
>
<NavbarStyle3Content dense={props.dense ? 1 : 0} folded={folded ? 1 : 0} />
</StyledNavBarMobile>
</Hidden>
</>
);
}
export default NavbarStyle3;

View File

@@ -0,0 +1,147 @@
import FuseScrollbars from '@fuse/core/FuseScrollbars';
import { styled, useTheme } from '@mui/material/styles';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import clsx from 'clsx';
import { memo, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import FuseNavigation from '@fuse/core/FuseNavigation';
import { navbarCloseMobile } from 'app/store/fuse/navbarSlice';
import { selectContrastMainTheme } from 'app/store/fuse/settingsSlice';
import { useLocation } from 'react-router-dom';
import useThemeMediaQuery from '@fuse/hooks/useThemeMediaQuery';
import { selectNavigation } from 'app/store/fuse/navigationSlice';
const Root = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
}));
const StyledPanel = styled(FuseScrollbars)(({ theme, opened }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
transition: theme.transitions.create(['opacity'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.shortest,
}),
opacity: 0,
pointerEvents: 'none',
...(opened && {
opacity: 1,
pointerEvents: 'initial',
}),
}));
function needsToBeOpened(location, item) {
return location && isUrlInChildren(item, location.pathname);
}
function isUrlInChildren(parent, url) {
if (!parent.children) {
return false;
}
for (let i = 0; i < parent.children.length; i += 1) {
if (parent.children[i].children) {
if (isUrlInChildren(parent.children[i], url)) {
return true;
}
}
if (parent.children[i].url === url || url.includes(parent.children[i].url)) {
return true;
}
}
return false;
}
function NavbarStyle3Content(props) {
const isMobile = useThemeMediaQuery((theme) => theme.breakpoints.down('lg'));
const navigation = useSelector(selectNavigation);
const [selectedNavigation, setSelectedNavigation] = useState([]);
const [panelOpen, setPanelOpen] = useState(false);
const theme = useTheme();
const dispatch = useDispatch();
const contrastTheme = useSelector(selectContrastMainTheme(theme.palette.primary.main));
const location = useLocation();
useEffect(() => {
navigation?.forEach((item) => {
if (needsToBeOpened(location, item)) {
setSelectedNavigation([item]);
}
});
}, [navigation, location]);
function handleParentItemClick(selected) {
/** if there is no child item do not set/open panel
*/
if (!selected.children) {
setSelectedNavigation([]);
setPanelOpen(false);
return;
}
/**
* If navigation already selected toggle panel visibility
*/
if (selectedNavigation[0]?.id === selected.id) {
setPanelOpen(!panelOpen);
} else {
/**
* Set navigation and open panel
*/
setSelectedNavigation([selected]);
setPanelOpen(true);
}
}
function handleChildItemClick(selected) {
setPanelOpen(false);
if (isMobile) {
dispatch(navbarCloseMobile());
}
}
return (
<ClickAwayListener onClickAway={() => setPanelOpen(false)}>
<Root className={clsx('flex flex-auto flex h-full', props.className)}>
<div id="fuse-navbar-side-panel" className="flex shrink-0 flex-col items-center">
<img className="w-44 my-32" src="assets/images/logo/logo.svg" alt="logo" />
<FuseScrollbars
className="flex flex-1 min-h-0 justify-center w-full overflow-y-auto overflow-x-hidden"
option={{ suppressScrollX: true, wheelPropagation: false }}
>
<FuseNavigation
className={clsx('navigation')}
navigation={navigation}
layout="vertical-2"
onItemClick={handleParentItemClick}
firstLevel
selectedId={selectedNavigation[0]?.id}
dense={props.dense}
/>
</FuseScrollbars>
</div>
{selectedNavigation.length > 0 && (
<StyledPanel
id="fuse-navbar-panel"
opened={panelOpen}
className={clsx('shadow-5 overflow-y-auto overflow-x-hidden')}
option={{ suppressScrollX: true, wheelPropagation: false }}
>
<FuseNavigation
className={clsx('navigation')}
navigation={selectedNavigation}
layout="vertical"
onItemClick={handleChildItemClick}
/>
</StyledPanel>
)}
</Root>
</ClickAwayListener>
);
}
export default memo(NavbarStyle3Content);

View File

@@ -0,0 +1,84 @@
import FuseDialog from '@fuse/core/FuseDialog';
import { styled } from '@mui/material/styles';
import FuseMessage from '@fuse/core/FuseMessage';
import FuseSuspense from '@fuse/core/FuseSuspense';
import AppContext from 'app/AppContext';
import clsx from 'clsx';
import { memo, useContext } from 'react';
import { useSelector } from 'react-redux';
import { useRoutes } from 'react-router-dom';
import { selectFuseCurrentLayoutConfig } from 'app/store/fuse/settingsSlice';
import FooterLayout2 from './components/FooterLayout2';
import LeftSideLayout2 from './components/LeftSideLayout2';
import NavbarWrapperLayout2 from './components/NavbarWrapperLayout2';
import RightSideLayout2 from './components/RightSideLayout2';
import ToolbarLayout2 from './components/ToolbarLayout2';
import SettingsPanel from '../shared-components/SettingsPanel';
const Root = styled('div')(({ theme, config }) => ({
...(config.mode === 'boxed' && {
clipPath: 'inset(0)',
maxWidth: `${config.containerWidth}px`,
margin: '0 auto',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
}),
...(config.mode === 'container' && {
'& .container': {
maxWidth: `${config.containerWidth}px`,
width: '100%',
margin: '0 auto',
},
}),
}));
function Layout2(props) {
const config = useSelector(selectFuseCurrentLayoutConfig);
const appContext = useContext(AppContext);
const { routes } = appContext;
return (
<Root id="fuse-layout" className="w-full flex" config={config}>
{config.leftSidePanel.display && <LeftSideLayout2 />}
<div className="flex flex-col flex-auto min-w-0">
<main id="fuse-main" className="flex flex-col flex-auto min-h-full min-w-0 relative">
{config.navbar.display && (
<NavbarWrapperLayout2
className={clsx(config.navbar.style === 'fixed' && 'sticky top-0 z-50')}
/>
)}
{config.toolbar.display && (
<ToolbarLayout2
className={clsx(
config.toolbar.style === 'fixed' && 'sticky top-0',
config.toolbar.position === 'above' && 'order-first z-40'
)}
/>
)}
<div className="sticky top-0 z-99">
<SettingsPanel />
</div>
<div className="flex flex-col flex-auto min-h-0 relative z-10">
<FuseDialog />
<FuseSuspense>{useRoutes(routes)}</FuseSuspense>
{props.children}
</div>
{config.footer.display && (
<FooterLayout2 className={config.footer.style === 'fixed' && 'sticky bottom-0'} />
)}
</main>
</div>
{config.rightSidePanel.display && <RightSideLayout2 />}
<FuseMessage />
</Root>
);
}
export default memo(Layout2);

View File

@@ -0,0 +1,138 @@
const config = {
title: 'Layout 2 - Horizontal',
defaults: {
mode: 'container',
containerWidth: 1120,
navbar: {
display: true,
style: 'fixed',
},
toolbar: {
display: true,
style: 'static',
position: 'below',
},
footer: {
display: true,
style: 'fixed',
},
leftSidePanel: {
display: true,
},
rightSidePanel: {
display: true,
},
},
form: {
mode: {
title: 'Mode',
type: 'radio',
options: [
{
name: 'Boxed',
value: 'boxed',
},
{
name: 'Full Width',
value: 'fullwidth',
},
{
name: 'Container',
value: 'container',
},
],
},
containerWidth: {
title: 'Container Width (px)',
type: 'number',
},
navbar: {
type: 'group',
title: 'Navbar',
children: {
display: {
title: 'Display',
type: 'switch',
},
style: {
title: 'Style',
type: 'radio',
options: [
{
name: 'Fixed',
value: 'fixed',
},
{
name: 'Static',
value: 'static',
},
],
},
},
},
toolbar: {
type: 'group',
title: 'Toolbar',
children: {
display: {
title: 'Display',
type: 'switch',
},
position: {
title: 'Position',
type: 'radio',
options: [
{
name: 'Above',
value: 'above',
},
{
name: 'Below',
value: 'below',
},
],
},
style: {
title: 'Style',
type: 'radio',
options: [
{
name: 'Fixed',
value: 'fixed',
},
{
name: 'Static',
value: 'static',
},
],
},
},
},
footer: {
type: 'group',
title: 'Footer',
children: {
display: {
title: 'Display',
type: 'switch',
},
style: {
title: 'Style',
type: 'radio',
options: [
{
name: 'Fixed',
value: 'fixed',
},
{
name: 'Static',
value: 'static',
},
],
},
},
},
},
};
export default config;

View File

@@ -0,0 +1,28 @@
import AppBar from '@mui/material/AppBar';
import { ThemeProvider } from '@mui/material/styles';
import Toolbar from '@mui/material/Toolbar';
import clsx from 'clsx';
import { memo } from 'react';
import { useSelector } from 'react-redux';
import { selectFooterTheme } from 'app/store/fuse/settingsSlice';
function FooterLayout2(props) {
const footerTheme = useSelector(selectFooterTheme);
return (
<ThemeProvider theme={footerTheme}>
<AppBar
id="fuse-footer"
className={clsx('relative z-20 shadow-md', props.className)}
color="default"
sx={{ backgroundColor: footerTheme.palette.background.paper }}
>
<Toolbar className="container min-h-48 md:min-h-64 px-8 sm:px-12 py-0 flex items-center overflow-x-auto">
Footer
</Toolbar>
</AppBar>
</ThemeProvider>
);
}
export default memo(FooterLayout2);

View File

@@ -0,0 +1,7 @@
import { memo } from 'react';
function LeftSideLayout2() {
return <></>;
}
export default memo(LeftSideLayout2);

View File

@@ -0,0 +1,29 @@
import FuseScrollbars from '@fuse/core/FuseScrollbars';
import { styled } from '@mui/material/styles';
import clsx from 'clsx';
import { memo } from 'react';
import Logo from '../../shared-components/Logo';
import Navigation from '../../shared-components/Navigation';
const Root = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
}));
function NavbarLayout2(props) {
return (
<Root className={clsx('w-full h-64 min-h-64 max-h-64 shadow-md', props.className)}>
<div className="flex flex-auto justify-between items-center w-full h-full container p-0 lg:px-24 z-20">
<div className="flex shrink-0 items-center px-8">
<Logo />
</div>
<FuseScrollbars className="flex h-full items-center">
<Navigation className="w-full" layout="horizontal" />
</FuseScrollbars>
</div>
</Root>
);
}
export default memo(NavbarLayout2);

View File

@@ -0,0 +1,63 @@
import FuseScrollbars from '@fuse/core/FuseScrollbars';
import { styled } from '@mui/material/styles';
import clsx from 'clsx';
import { memo } from 'react';
import UserNavbarHeader from '../../shared-components/UserNavbarHeader';
import NavbarToggleButton from '../../shared-components/NavbarToggleButton';
import Logo from '../../shared-components/Logo';
import Navigation from '../../shared-components/Navigation';
const Root = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
'& ::-webkit-scrollbar-thumb': {
boxShadow: `inset 0 0 0 20px ${
theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.24)' : 'rgba(255, 255, 255, 0.24)'
}`,
},
'& ::-webkit-scrollbar-thumb:active': {
boxShadow: `inset 0 0 0 20px ${
theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.37)' : 'rgba(255, 255, 255, 0.37)'
}`,
},
}));
const StyledContent = styled(FuseScrollbars)(({ theme }) => ({
overscrollBehavior: 'contain',
overflowX: 'hidden',
overflowY: 'auto',
WebkitOverflowScrolling: 'touch',
backgroundRepeat: 'no-repeat',
backgroundSize: '100% 40px, 100% 10px',
backgroundAttachment: 'local, scroll',
}));
function NavbarMobileLayout2(props) {
return (
<Root className={clsx('flex flex-col h-full overflow-hidden', props.className)}>
<div className="flex flex-row items-center shrink-0 h-48 md:h-72 px-20">
<div className="flex flex-1 mx-4">
<Logo />
</div>
<NavbarToggleButton className="w-40 h-40 p-0" />
</div>
<StyledContent
className="flex flex-1 flex-col min-h-0"
option={{ suppressScrollX: true, wheelPropagation: false }}
>
<UserNavbarHeader />
<Navigation layout="vertical" />
<div className="flex flex-0 items-center justify-center py-48 opacity-10">
<img className="w-full max-w-64" src="assets/images/logo/logo.svg" alt="footer logo" />
</div>
</StyledContent>
</Root>
);
}
export default memo(NavbarMobileLayout2);

View File

@@ -0,0 +1,64 @@
import Hidden from '@mui/material/Hidden';
import { styled, ThemeProvider } from '@mui/material/styles';
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
import { navbarCloseMobile, selectFuseNavbar } from 'app/store/fuse/navbarSlice';
import { memo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { selectFuseCurrentLayoutConfig, selectNavbarTheme } from 'app/store/fuse/settingsSlice';
import NavbarLayout2 from './NavbarLayout2';
import NavbarMobileLayout2 from './NavbarMobileLayout2';
import NavbarToggleFab from '../../shared-components/NavbarToggleFab';
const StyledSwipeableDrawer = styled(SwipeableDrawer)(({ theme }) => ({
'& > .MuiDrawer-paper': {
height: '100%',
flexDirection: 'column',
flex: '1 1 auto',
width: 280,
minWidth: 280,
transition: theme.transitions.create(['width', 'min-width'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.shorter,
}),
},
}));
function NavbarWrapperLayout2(props) {
const dispatch = useDispatch();
const config = useSelector(selectFuseCurrentLayoutConfig);
const navbarTheme = useSelector(selectNavbarTheme);
const navbar = useSelector(selectFuseNavbar);
return (
<>
<ThemeProvider theme={navbarTheme}>
<Hidden lgDown>
<NavbarLayout2 />
</Hidden>
<Hidden lgUp>
<StyledSwipeableDrawer
anchor="left"
variant="temporary"
open={navbar.mobileOpen}
onClose={() => dispatch(navbarCloseMobile())}
onOpen={() => {}}
disableSwipeToOpen
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
>
<NavbarMobileLayout2 />
</StyledSwipeableDrawer>
</Hidden>
</ThemeProvider>
{config.navbar.display && !config.toolbar.display && (
<Hidden lgUp>
<NavbarToggleFab />
</Hidden>
)}
</>
);
}
export default memo(NavbarWrapperLayout2);

View File

@@ -0,0 +1,15 @@
import { memo } from 'react';
import QuickPanel from '../../shared-components/quickPanel/QuickPanel';
import NotificationPanel from '../../shared-components/notificationPanel/NotificationPanel';
function RightSideLayout2() {
return (
<>
<QuickPanel />
<NotificationPanel />
</>
);
}
export default memo(RightSideLayout2);

View File

@@ -0,0 +1,70 @@
import { ThemeProvider } from '@mui/material/styles';
import AppBar from '@mui/material/AppBar';
import Hidden from '@mui/material/Hidden';
import Toolbar from '@mui/material/Toolbar';
import clsx from 'clsx';
import { memo } from 'react';
import { useSelector } from 'react-redux';
import { selectFuseCurrentLayoutConfig, selectToolbarTheme } from 'app/store/fuse/settingsSlice';
import AdjustFontSize from '../../shared-components/AdjustFontSize';
import FullScreenToggle from '../../shared-components/FullScreenToggle';
import LanguageSwitcher from '../../shared-components/LanguageSwitcher';
import NotificationPanelToggleButton from '../../shared-components/notificationPanel/NotificationPanelToggleButton';
import NavigationShortcuts from '../../shared-components/NavigationShortcuts';
import NavigationSearch from '../../shared-components/NavigationSearch';
import NavbarToggleButton from '../../shared-components/NavbarToggleButton';
import UserMenu from '../../shared-components/UserMenu';
import QuickPanelToggleButton from '../../shared-components/quickPanel/QuickPanelToggleButton';
import ChatPanelToggleButton from '../../shared-components/chatPanel/ChatPanelToggleButton';
function ToolbarLayout2(props) {
const config = useSelector(selectFuseCurrentLayoutConfig);
const toolbarTheme = useSelector(selectToolbarTheme);
return (
<ThemeProvider theme={toolbarTheme}>
<AppBar
id="fuse-toolbar"
className={clsx('flex relative z-20 shadow-md', props.className)}
color="default"
style={{ backgroundColor: toolbarTheme.palette.background.paper }}
>
<Toolbar className="container p-0 lg:px-24 min-h-48 md:min-h-64">
{config.navbar.display && (
<Hidden lgUp>
<NavbarToggleButton className="w-40 h-40 p-0 mx-0 sm:mx-8" />
</Hidden>
)}
<div className="flex flex-1">
<Hidden lgDown>
<NavigationShortcuts />
</Hidden>
</div>
<div className="flex items-center px-8 h-full overflow-x-auto">
<LanguageSwitcher />
<AdjustFontSize />
<FullScreenToggle />
<NavigationSearch />
<Hidden lgUp>
<ChatPanelToggleButton />
</Hidden>
<QuickPanelToggleButton />
<NotificationPanelToggleButton />
<UserMenu />
</div>
</Toolbar>
</AppBar>
</ThemeProvider>
);
}
export default memo(ToolbarLayout2);

View File

@@ -0,0 +1,83 @@
import FuseDialog from '@fuse/core/FuseDialog';
import { styled } from '@mui/material/styles';
import FuseMessage from '@fuse/core/FuseMessage';
import FuseSuspense from '@fuse/core/FuseSuspense';
import clsx from 'clsx';
import { memo, useContext } from 'react';
import { useSelector } from 'react-redux';
import { useRoutes } from 'react-router-dom';
import AppContext from 'app/AppContext';
import { selectFuseCurrentLayoutConfig } from 'app/store/fuse/settingsSlice';
import FooterLayout3 from './components/FooterLayout3';
import LeftSideLayout3 from './components/LeftSideLayout3';
import NavbarWrapperLayout3 from './components/NavbarWrapperLayout3';
import RightSideLayout3 from './components/RightSideLayout3';
import ToolbarLayout3 from './components/ToolbarLayout3';
import SettingsPanel from '../shared-components/SettingsPanel';
const Root = styled('div')(({ theme, config }) => ({
...(config.mode === 'boxed' && {
clipPath: 'inset(0)',
maxWidth: `${config.containerWidth}px`,
margin: '0 auto',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
}),
...(config.mode === 'container' && {
'& .container': {
maxWidth: `${config.containerWidth}px`,
width: '100%',
margin: '0 auto',
},
}),
}));
function Layout3(props) {
const config = useSelector(selectFuseCurrentLayoutConfig);
const appContext = useContext(AppContext);
const { routes } = appContext;
return (
<Root id="fuse-layout" className="w-full flex" config={config}>
{config.leftSidePanel.display && <LeftSideLayout3 />}
<div className="flex flex-col flex-auto min-w-0">
<main id="fuse-main" className="flex flex-col flex-auto min-h-full min-w-0 relative">
{config.navbar.display && (
<NavbarWrapperLayout3
className={clsx(config.navbar.style === 'fixed' && 'sticky top-0 z-50')}
/>
)}
{config.toolbar.display && (
<ToolbarLayout3
className={clsx(
config.toolbar.style === 'fixed' && 'sticky top-0',
config.toolbar.position === 'above' && 'order-first z-40'
)}
/>
)}
<div className="sticky top-0 z-99">
<SettingsPanel />
</div>
<div className="flex flex-col flex-auto min-h-0 relative z-10">
<FuseDialog />
<FuseSuspense>{useRoutes(routes)}</FuseSuspense>
{props.children}
</div>
{config.footer.display && (
<FooterLayout3 className={config.footer.style === 'fixed' && 'sticky bottom-0'} />
)}
</main>
</div>
{config.rightSidePanel.display && <RightSideLayout3 />}
<FuseMessage />
</Root>
);
}
export default memo(Layout3);

View File

@@ -0,0 +1,139 @@
const config = {
title: 'Layout 3 - Horizontal',
defaults: {
mode: 'container',
containerWidth: 1120,
scroll: 'content',
navbar: {
display: true,
style: 'fixed',
},
toolbar: {
display: true,
style: 'static',
position: 'below',
},
footer: {
display: true,
style: 'fixed',
},
leftSidePanel: {
display: true,
},
rightSidePanel: {
display: true,
},
},
form: {
mode: {
title: 'Mode',
type: 'radio',
options: [
{
name: 'Boxed',
value: 'boxed',
},
{
name: 'Full Width',
value: 'fullwidth',
},
{
name: 'Container',
value: 'container',
},
],
},
containerWidth: {
title: 'Container Width (px)',
type: 'number',
},
navbar: {
type: 'group',
title: 'Navbar',
children: {
display: {
title: 'Display',
type: 'switch',
},
style: {
title: 'Style',
type: 'radio',
options: [
{
name: 'Fixed',
value: 'fixed',
},
{
name: 'Static',
value: 'static',
},
],
},
},
},
toolbar: {
type: 'group',
title: 'Toolbar',
children: {
display: {
title: 'Display',
type: 'switch',
},
position: {
title: 'Position',
type: 'radio',
options: [
{
name: 'Above',
value: 'above',
},
{
name: 'Below',
value: 'below',
},
],
},
style: {
title: 'Style',
type: 'radio',
options: [
{
name: 'Fixed',
value: 'fixed',
},
{
name: 'Static',
value: 'static',
},
],
},
},
},
footer: {
type: 'group',
title: 'Footer',
children: {
display: {
title: 'Display',
type: 'switch',
},
style: {
title: 'Style',
type: 'radio',
options: [
{
name: 'Fixed',
value: 'fixed',
},
{
name: 'Static',
value: 'static',
},
],
},
},
},
},
};
export default config;

View File

@@ -0,0 +1,28 @@
import AppBar from '@mui/material/AppBar';
import { ThemeProvider } from '@mui/material/styles';
import Toolbar from '@mui/material/Toolbar';
import clsx from 'clsx';
import { memo } from 'react';
import { useSelector } from 'react-redux';
import { selectFooterTheme } from 'app/store/fuse/settingsSlice';
function FooterLayout3(props) {
const footerTheme = useSelector(selectFooterTheme);
return (
<ThemeProvider theme={footerTheme}>
<AppBar
id="fuse-footer"
className={clsx('relative z-20 shadow-md', props.className)}
color="default"
style={{ backgroundColor: footerTheme.palette.background.paper }}
>
<Toolbar className="container min-h-48 md:min-h-64 px-8 sm:px-12 lg:px-20 py-0 flex items-center overflow-x-auto">
Footer
</Toolbar>
</AppBar>
</ThemeProvider>
);
}
export default memo(FooterLayout3);

View File

@@ -0,0 +1,15 @@
import FuseSidePanel from '@fuse/core/FuseSidePanel';
import { memo } from 'react';
import NavigationShortcuts from '../../shared-components/NavigationShortcuts';
function LeftSideLayout3() {
return (
<>
<FuseSidePanel>
<NavigationShortcuts className="py-16 px-8" variant="vertical" />
</FuseSidePanel>
</>
);
}
export default memo(LeftSideLayout3);

View File

@@ -0,0 +1,24 @@
import FuseScrollbars from '@fuse/core/FuseScrollbars';
import { styled } from '@mui/material/styles';
import clsx from 'clsx';
import { memo } from 'react';
import Navigation from '../../shared-components/Navigation';
const Root = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
}));
function NavbarLayout3(props) {
return (
<Root className={clsx('w-full h-64 min-h-64 max-h-64 shadow-md', props.className)}>
<div className="flex flex-auto items-center w-full h-full container px-16 lg:px-24">
<FuseScrollbars className="flex h-full items-center">
<Navigation className="w-full" layout="horizontal" dense />
</FuseScrollbars>
</div>
</Root>
);
}
export default memo(NavbarLayout3);

View File

@@ -0,0 +1,63 @@
import FuseScrollbars from '@fuse/core/FuseScrollbars';
import { styled } from '@mui/material/styles';
import clsx from 'clsx';
import { memo } from 'react';
import Navigation from '../../shared-components/Navigation';
import UserNavbarHeader from '../../shared-components/UserNavbarHeader';
import NavbarToggleButton from '../../shared-components/NavbarToggleButton';
import Logo from '../../shared-components/Logo';
const Root = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
'& ::-webkit-scrollbar-thumb': {
boxShadow: `inset 0 0 0 20px ${
theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.24)' : 'rgba(255, 255, 255, 0.24)'
}`,
},
'& ::-webkit-scrollbar-thumb:active': {
boxShadow: `inset 0 0 0 20px ${
theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.37)' : 'rgba(255, 255, 255, 0.37)'
}`,
},
}));
const StyledContent = styled(FuseScrollbars)(({ theme }) => ({
overscrollBehavior: 'contain',
overflowX: 'hidden',
overflowY: 'auto',
WebkitOverflowScrolling: 'touch',
backgroundRepeat: 'no-repeat',
backgroundSize: '100% 40px, 100% 10px',
backgroundAttachment: 'local, scroll',
}));
function NavbarMobileLayout3(props) {
return (
<Root className={clsx('flex flex-col h-full overflow-hidden', props.className)}>
<div className="flex flex-row items-center shrink-0 h-48 md:h-72 px-20">
<div className="flex flex-1 mx-4">
<Logo />
</div>
<NavbarToggleButton className="w-40 h-40 p-0" />
</div>
<StyledContent
className="flex flex-1 flex-col min-h-0"
option={{ suppressScrollX: true, wheelPropagation: false }}
>
<UserNavbarHeader />
<Navigation layout="vertical" />
<div className="flex flex-0 items-center justify-center py-48 opacity-10">
<img className="w-full max-w-64" src="assets/images/logo/logo.svg" alt="footer logo" />
</div>
</StyledContent>
</Root>
);
}
export default memo(NavbarMobileLayout3);

View File

@@ -0,0 +1,65 @@
import Hidden from '@mui/material/Hidden';
import { styled, ThemeProvider } from '@mui/material/styles';
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
import { navbarCloseMobile, selectFuseNavbar } from 'app/store/fuse/navbarSlice';
import clsx from 'clsx';
import { memo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { selectFuseCurrentLayoutConfig, selectNavbarTheme } from 'app/store/fuse/settingsSlice';
import NavbarLayout3 from './NavbarLayout3';
import NavbarMobileLayout3 from './NavbarMobileLayout3';
import NavbarToggleFab from '../../shared-components/NavbarToggleFab';
const StyledSwipeableDrawer = styled(SwipeableDrawer)(({ theme }) => ({
'& > .MuiDrawer-paper': {
height: '100%',
flexDirection: 'column',
flex: '1 1 auto',
width: 280,
minWidth: 280,
transition: theme.transitions.create(['width', 'min-width'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.shorter,
}),
},
}));
function NavbarWrapperLayout3(props) {
const dispatch = useDispatch();
const config = useSelector(selectFuseCurrentLayoutConfig);
const navbarTheme = useSelector(selectNavbarTheme);
const navbar = useSelector(selectFuseNavbar);
return (
<>
<ThemeProvider theme={navbarTheme}>
<Hidden lgDown>
<NavbarLayout3 className={clsx(props.className)} />
</Hidden>
<Hidden lgUp>
<StyledSwipeableDrawer
anchor="left"
variant="temporary"
open={navbar.mobileOpen}
onClose={() => dispatch(navbarCloseMobile())}
onOpen={() => {}}
disableSwipeToOpen
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
>
<NavbarMobileLayout3 />
</StyledSwipeableDrawer>
</Hidden>
</ThemeProvider>
{config.navbar.display && !config.toolbar.display && (
<Hidden lgUp>
<NavbarToggleFab />
</Hidden>
)}
</>
);
}
export default memo(NavbarWrapperLayout3);

View File

@@ -0,0 +1,15 @@
import { memo } from 'react';
import NotificationPanel from '../../shared-components/notificationPanel/NotificationPanel';
import QuickPanel from '../../shared-components/quickPanel/QuickPanel';
function RightSideLayout3() {
return (
<>
<QuickPanel />
<NotificationPanel />
</>
);
}
export default memo(RightSideLayout3);

View File

@@ -0,0 +1,78 @@
import { ThemeProvider } from '@mui/material/styles';
import AppBar from '@mui/material/AppBar';
import Hidden from '@mui/material/Hidden';
import Toolbar from '@mui/material/Toolbar';
import clsx from 'clsx';
import { memo } from 'react';
import { useSelector } from 'react-redux';
import { selectFuseCurrentLayoutConfig, selectToolbarTheme } from 'app/store/fuse/settingsSlice';
import AdjustFontSize from '../../shared-components/AdjustFontSize';
import FullScreenToggle from '../../shared-components/FullScreenToggle';
import LanguageSwitcher from '../../shared-components/LanguageSwitcher';
import NotificationPanelToggleButton from '../../shared-components/notificationPanel/NotificationPanelToggleButton';
import NavigationSearch from '../../shared-components/NavigationSearch';
import UserMenu from '../../shared-components/UserMenu';
import QuickPanelToggleButton from '../../shared-components/quickPanel/QuickPanelToggleButton';
import ChatPanelToggleButton from '../../shared-components/chatPanel/ChatPanelToggleButton';
import Logo from '../../shared-components/Logo';
import NavbarToggleButton from '../../shared-components/NavbarToggleButton';
function ToolbarLayout3(props) {
const config = useSelector(selectFuseCurrentLayoutConfig);
const toolbarTheme = useSelector(selectToolbarTheme);
return (
<ThemeProvider theme={toolbarTheme}>
<AppBar
id="fuse-toolbar"
className={clsx('flex relative z-20 shadow-md', props.className)}
color="default"
style={{ backgroundColor: toolbarTheme.palette.background.paper }}
>
<Toolbar className="container p-0 lg:px-24 min-h-48 md:min-h-64">
{config.navbar.display && (
<Hidden lgUp>
<NavbarToggleButton className="w-40 h-40 p-0 mx-0 sm:mx-8" />
</Hidden>
)}
<Hidden lgDown>
<div className={clsx('flex shrink-0 items-center')}>
<Logo />
</div>
</Hidden>
<div className="flex flex-1">
<Hidden smDown>
<NavigationSearch className="mx-16 lg:mx-24" variant="basic" />
</Hidden>
</div>
<div className="flex items-center px-8 md:px-0 h-full overflow-x-auto">
<Hidden smUp>
<NavigationSearch />
</Hidden>
<Hidden lgUp>
<ChatPanelToggleButton />
</Hidden>
<LanguageSwitcher />
<AdjustFontSize />
<FullScreenToggle />
<QuickPanelToggleButton />
<NotificationPanelToggleButton />
<UserMenu />
</div>
</Toolbar>
</AppBar>
</ThemeProvider>
);
}
export default memo(ToolbarLayout3);

View File

@@ -0,0 +1,89 @@
import { useState } from 'react';
import Slider from '@mui/material/Slider';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import Menu from '@mui/material/Menu';
import clsx from 'clsx';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
const marks = [
{ value: 0.7, label: '70%' },
{ value: 0.8, label: '80%' },
{ value: 0.9, label: '90%' },
{ value: 1, label: '100%' },
{ value: 1.1, label: '110%' },
{ value: 1.2, label: '120%' },
{ value: 1.3, label: '130%' },
];
function AdjustFontSize(props) {
const [anchorEl, setAnchorEl] = useState(null);
const [fontSize, setFontSize] = useState(1);
function changeHtmlFontSize() {
const html = document.getElementsByTagName('html')[0];
html.style.fontSize = `${fontSize * 62.5}%`;
}
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
return (
<div>
<IconButton
className={clsx('w-40 h-40', props.className)}
aria-controls="font-size-menu"
aria-haspopup="true"
onClick={handleClick}
size="large"
>
<FuseSvgIcon>material-outline:format_size</FuseSvgIcon>
</IconButton>
<Menu
classes={{ paper: 'w-320' }}
id="font-size-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
>
<div className="py-12 px-24">
<Typography className="flex items-center justify-center text-16 font-semibold mb-8">
<FuseSvgIcon color="action" className="mr-4">
material-outline:format_size
</FuseSvgIcon>
Font Size
</Typography>
<Slider
classes={{ markLabel: 'text-12 font-semibold' }}
value={fontSize}
track={false}
aria-labelledby="discrete-slider-small-steps"
step={0.1}
marks={marks}
min={0.7}
max={1.3}
valueLabelDisplay="off"
onChange={(ev, value) => setFontSize(value)}
onChangeCommitted={changeHtmlFontSize}
/>
</div>
</Menu>
</div>
);
}
export default AdjustFontSize;

View File

@@ -0,0 +1,21 @@
import Button from '@mui/material/Button';
import { Link } from 'react-router-dom';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
function DocumentationButton({ className }) {
return (
<Button
component={Link}
to="/documentation"
role="button"
className={className}
variant="contained"
color="primary"
startIcon={<FuseSvgIcon size={16}>heroicons-outline:book-open</FuseSvgIcon>}
>
Documentation
</Button>
);
}
export default DocumentationButton;

View File

@@ -0,0 +1,96 @@
import Tooltip from '@mui/material/Tooltip';
import clsx from 'clsx';
import { useEffect, useLayoutEffect, useState } from 'react';
import IconButton from '@mui/material/IconButton';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
const useEnhancedEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
const HeaderFullScreenToggle = (props) => {
const [isFullScreen, setIsFullScreen] = useState(false);
useEnhancedEffect(() => {
document.onfullscreenchange = () =>
setIsFullScreen(document[getBrowserFullscreenElementProp()] != null);
return () => {
document.onfullscreenchange = undefined;
};
});
function getBrowserFullscreenElementProp() {
if (typeof document.fullscreenElement !== 'undefined') {
return 'fullscreenElement';
}
if (typeof document.mozFullScreenElement !== 'undefined') {
return 'mozFullScreenElement';
}
if (typeof document.msFullscreenElement !== 'undefined') {
return 'msFullscreenElement';
}
if (typeof document.webkitFullscreenElement !== 'undefined') {
return 'webkitFullscreenElement';
}
throw new Error('fullscreenElement is not supported by this browser');
}
/* View in fullscreen */
function openFullscreen() {
const elem = document.documentElement;
if (elem.requestFullscreen) {
elem.requestFullscreen();
} else if (elem.mozRequestFullScreen) {
/* Firefox */
elem.mozRequestFullScreen();
} else if (elem.webkitRequestFullscreen) {
/* Chrome, Safari and Opera */
elem.webkitRequestFullscreen();
} else if (elem.msRequestFullscreen) {
/* IE/Edge */
elem.msRequestFullscreen();
}
}
/* Close fullscreen */
function closeFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
/* Firefox */
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
/* Chrome, Safari and Opera */
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
/* IE/Edge */
document.msExitFullscreen();
}
}
function toggleFullScreen() {
if (
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement
) {
closeFullscreen();
} else {
openFullscreen();
}
}
return (
<Tooltip title="Fullscreen toggle" placement="bottom">
<IconButton
onClick={toggleFullScreen}
className={clsx('w-40 h-40', props.className)}
size="large"
>
<FuseSvgIcon>heroicons-outline:arrows-expand</FuseSvgIcon>
</IconButton>
</Tooltip>
);
};
export default HeaderFullScreenToggle;

View File

@@ -0,0 +1,65 @@
import { useState } from 'react';
import clsx from 'clsx';
import Button from '@mui/material/Button';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import Dialog from '@mui/material/Dialog';
import { useSelector } from 'react-redux';
import { selectFuseCurrentSettings } from 'app/store/fuse/settingsSlice';
import FuseHighlight from '@fuse/core/FuseHighlight';
import DialogTitle from '@mui/material/DialogTitle';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import qs from 'qs';
import Typography from '@mui/material/Typography';
function FuseSettingsViewerDialog(props) {
const { className } = props;
const [openDialog, setOpenDialog] = useState(false);
const settings = useSelector(selectFuseCurrentSettings);
function handleOpenDialog() {
setOpenDialog(true);
}
function handleCloseDialog() {
setOpenDialog(false);
}
return (
<div className={clsx('', className)}>
<Button
variant="contained"
color="secondary"
className="w-full"
onClick={handleOpenDialog}
startIcon={<FuseSvgIcon>heroicons-solid:code</FuseSvgIcon>}
>
View settings as json/query params
</Button>
<Dialog open={openDialog} onClose={handleCloseDialog} aria-labelledby="form-dialog-title">
<DialogTitle className="">Fuse Settings Viewer</DialogTitle>
<DialogContent className="">
<Typography className="text-16 font-bold mt-24 mb-16">JSON</Typography>
<FuseHighlight component="pre" className="language-json">
{JSON.stringify(settings, null, 2)}
</FuseHighlight>
<Typography className="text-16 font-bold mt-24 mb-16">Query Params</Typography>
{qs.stringify({
defaultSettings: JSON.stringify(settings, { strictNullHandling: true }),
})}
</DialogContent>
<DialogActions>
<Button color="secondary" variant="contained" onClick={handleCloseDialog}>
Close
</Button>
</DialogActions>
</Dialog>
</div>
);
}
export default FuseSettingsViewerDialog;

View File

@@ -0,0 +1,88 @@
import Button from '@mui/material/Button';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import MenuItem from '@mui/material/MenuItem';
import Popover from '@mui/material/Popover';
import Typography from '@mui/material/Typography';
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { changeLanguage, selectCurrentLanguage, selectLanguages } from 'app/store/i18nSlice';
function LanguageSwitcher(props) {
const currentLanguage = useSelector(selectCurrentLanguage);
const languages = useSelector(selectLanguages);
const [menu, setMenu] = useState(null);
const dispatch = useDispatch();
const langMenuClick = (event) => {
setMenu(event.currentTarget);
};
const langMenuClose = () => {
setMenu(null);
};
function handleLanguageChange(lng) {
dispatch(changeLanguage(lng.id));
langMenuClose();
}
return (
<>
<Button className="h-40 w-64" onClick={langMenuClick}>
<img
className="mx-4 min-w-20"
src={`assets/images/flags/${currentLanguage.flag}.svg`}
alt={currentLanguage.title}
/>
<Typography className="mx-4 font-semibold uppercase" color="text.secondary">
{currentLanguage.id}
</Typography>
</Button>
<Popover
open={Boolean(menu)}
anchorEl={menu}
onClose={langMenuClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
classes={{
paper: 'py-8',
}}
>
{languages.map((lng) => (
<MenuItem key={lng.id} onClick={() => handleLanguageChange(lng)}>
<ListItemIcon className="min-w-40">
<img
className="min-w-20"
src={`assets/images/flags/${lng.flag}.svg`}
alt={lng.title}
/>
</ListItemIcon>
<ListItemText primary={lng.title} />
</MenuItem>
))}
<MenuItem
component={Link}
to="/documentation/configuration/multi-language"
onClick={langMenuClose}
role="button"
>
<ListItemText primary="Learn More" />
</MenuItem>
</Popover>
</>
);
}
export default LanguageSwitcher;

View File

@@ -0,0 +1,39 @@
import { styled } from '@mui/material/styles';
const Root = styled('div')(({ theme }) => ({
'& > .logo-icon': {
transition: theme.transitions.create(['width', 'height'], {
duration: theme.transitions.duration.shortest,
easing: theme.transitions.easing.easeInOut,
}),
},
'& > .badge': {
transition: theme.transitions.create('opacity', {
duration: theme.transitions.duration.shortest,
easing: theme.transitions.easing.easeInOut,
}),
},
}));
function Logo() {
return (
<Root className="flex items-center">
<img className="logo-icon w-32 h-32" src="assets/images/logo/logo.svg" alt="logo" />
<div
className="badge flex items-center py-4 px-8 mx-8 rounded"
style={{ backgroundColor: '#121212', color: '#61DAFB' }}
>
<img
className="react-badge"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xMS41IC0xMC4yMzE3NCAyMyAyMC40NjM0OCI+CiAgPHRpdGxlPlJlYWN0IExvZ288L3RpdGxlPgogIDxjaXJjbGUgY3g9IjAiIGN5PSIwIiByPSIyLjA1IiBmaWxsPSIjNjFkYWZiIi8+CiAgPGcgc3Ryb2tlPSIjNjFkYWZiIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiPgogICAgPGVsbGlwc2Ugcng9IjExIiByeT0iNC4yIi8+CiAgICA8ZWxsaXBzZSByeD0iMTEiIHJ5PSI0LjIiIHRyYW5zZm9ybT0icm90YXRlKDYwKSIvPgogICAgPGVsbGlwc2Ugcng9IjExIiByeT0iNC4yIiB0cmFuc2Zvcm09InJvdGF0ZSgxMjApIi8+CiAgPC9nPgo8L3N2Zz4K"
alt="react"
width="16"
/>
<span className="react-text text-12 mx-4">React</span>
</div>
</Root>
);
}
export default Logo;

View File

@@ -0,0 +1,47 @@
import IconButton from '@mui/material/IconButton';
import { useDispatch, useSelector } from 'react-redux';
import { selectFuseCurrentSettings, setDefaultSettings } from 'app/store/fuse/settingsSlice';
import _ from '@lodash';
import useThemeMediaQuery from '@fuse/hooks/useThemeMediaQuery';
import { navbarToggle, navbarToggleMobile } from 'app/store/fuse/navbarSlice';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
function NavbarToggleButton(props) {
const dispatch = useDispatch();
const isMobile = useThemeMediaQuery((theme) => theme.breakpoints.down('lg'));
const settings = useSelector(selectFuseCurrentSettings);
const { config } = settings.layout;
return (
<IconButton
className={props.className}
color="inherit"
size="small"
onClick={(ev) => {
if (isMobile) {
dispatch(navbarToggleMobile());
} else if (config.navbar.style === 'style-2') {
dispatch(
setDefaultSettings(
_.set({}, 'layout.config.navbar.folded', !settings.layout.config.navbar.folded)
)
);
} else {
dispatch(navbarToggle());
}
}}
>
{props.children}
</IconButton>
);
}
NavbarToggleButton.defaultProps = {
children: (
<FuseSvgIcon size={20} color="action">
heroicons-outline:view-list
</FuseSvgIcon>
),
};
export default NavbarToggleButton;

View File

@@ -0,0 +1,89 @@
import Fab from '@mui/material/Fab';
import { styled } from '@mui/material/styles';
import Tooltip from '@mui/material/Tooltip';
import { navbarToggle, navbarToggleMobile } from 'app/store/fuse/navbarSlice';
import clsx from 'clsx';
import { useDispatch, useSelector } from 'react-redux';
import useThemeMediaQuery from '@fuse/hooks/useThemeMediaQuery';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import { selectFuseCurrentLayoutConfig } from 'app/store/fuse/settingsSlice';
const Root = styled(Tooltip)(({ theme, position }) => ({
'& > .button': {
height: 40,
position: 'absolute',
zIndex: 99,
top: 12,
width: 24,
borderRadius: 38,
padding: 8,
backgroundColor: theme.palette.background.paper,
transition: theme.transitions.create(
['background-color', 'border-radius', 'width', 'min-width', 'padding'],
{
easing: theme.transitions.easing.easeInOut,
duration: theme.transitions.duration.shorter,
}
),
'&:hover': {
width: 52,
paddingLeft: 8,
paddingRight: 8,
},
'& > .button-icon': {
fontSize: 18,
transition: theme.transitions.create(['transform'], {
easing: theme.transitions.easing.easeInOut,
duration: theme.transitions.duration.short,
}),
},
...(position === 'left' && {
borderBottomLeftRadius: 0,
borderTopLeftRadius: 0,
paddingLeft: 4,
left: 0,
}),
...(position === 'right' && {
borderBottomRightRadius: 0,
borderTopRightRadius: 0,
paddingRight: 4,
right: 0,
'& > .button-icon': {
transform: 'rotate(-180deg)',
},
}),
},
}));
function NavbarToggleFab(props) {
const isMobile = useThemeMediaQuery((theme) => theme.breakpoints.down('lg'));
const config = useSelector(selectFuseCurrentLayoutConfig);
const dispatch = useDispatch();
return (
<Root
title="Show Navigation"
placement={config.navbar.position === 'left' ? 'right' : 'left'}
position={config.navbar.position}
>
<Fab
className={clsx('button', props.className)}
onClick={(ev) => dispatch(isMobile ? navbarToggleMobile() : navbarToggle())}
disableRipple
>
<FuseSvgIcon color="action" className="button-icon">
heroicons-outline:view-list
</FuseSvgIcon>
</Fab>
</Root>
);
}
NavbarToggleFab.defaultProps = {};
export default NavbarToggleFab;

View File

@@ -0,0 +1,39 @@
import FuseNavigation from '@fuse/core/FuseNavigation';
import clsx from 'clsx';
import { memo, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { selectNavigation } from 'app/store/fuse/navigationSlice';
import useThemeMediaQuery from '@fuse/hooks/useThemeMediaQuery';
import { navbarCloseMobile } from 'app/store/fuse/navbarSlice';
function Navigation(props) {
const navigation = useSelector(selectNavigation);
const isMobile = useThemeMediaQuery((theme) => theme.breakpoints.down('lg'));
const dispatch = useDispatch();
return useMemo(() => {
function handleItemClick(item) {
if (isMobile) {
dispatch(navbarCloseMobile());
}
}
return (
<FuseNavigation
className={clsx('navigation', props.className)}
navigation={navigation}
layout={props.layout}
dense={props.dense}
active={props.active}
onItemClick={handleItemClick}
/>
);
}, [dispatch, isMobile, navigation, props.active, props.className, props.dense, props.layout]);
}
Navigation.defaultProps = {
layout: 'vertical',
};
export default memo(Navigation);

View File

@@ -0,0 +1,12 @@
import { useSelector } from 'react-redux';
import FuseSearch from '@fuse/core/FuseSearch';
import { selectFlatNavigation } from 'app/store/fuse/navigationSlice';
function NavigationSearch(props) {
const { variant, className } = props;
const navigation = useSelector(selectFlatNavigation);
return <FuseSearch className={className} variant={variant} navigation={navigation} />;
}
export default NavigationSearch;

View File

@@ -0,0 +1,27 @@
import { useDispatch, useSelector } from 'react-redux';
import FuseShortcuts from '@fuse/core/FuseShortcuts';
import { selectFlatNavigation } from 'app/store/fuse/navigationSlice';
import { selectUserShortcuts, updateUserShortcuts } from 'app/store/userSlice';
function NavigationShortcuts(props) {
const { variant, className } = props;
const dispatch = useDispatch();
const shortcuts = useSelector(selectUserShortcuts) || [];
const navigation = useSelector(selectFlatNavigation);
function handleShortcutsChange(newShortcuts) {
dispatch(updateUserShortcuts(newShortcuts));
}
return (
<FuseShortcuts
className={className}
variant={variant}
navigation={navigation}
shortcuts={shortcuts}
onChange={handleShortcutsChange}
/>
);
}
export default NavigationShortcuts;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,23 @@
import Button from '@mui/material/Button';
import clsx from 'clsx';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
function PurchaseButton({ className }) {
return (
<Button
component="a"
href="https://1.envato.market/zDGL6"
target="_blank"
rel="noreferrer noopener"
role="button"
className={clsx('', className)}
variant="contained"
color="secondary"
startIcon={<FuseSvgIcon size={16}>heroicons-outline:shopping-cart</FuseSvgIcon>}
>
Purchase FUSE React
</Button>
);
}
export default PurchaseButton;

View File

@@ -0,0 +1,202 @@
import FuseScrollbars from '@fuse/core/FuseScrollbars';
import { styled, useTheme } from '@mui/material/styles';
import FuseSettings from '@fuse/core/FuseSettings';
import Button from '@mui/material/Button';
import { red } from '@mui/material/colors';
import Dialog from '@mui/material/Dialog';
import IconButton from '@mui/material/IconButton';
import Slide from '@mui/material/Slide';
import Typography from '@mui/material/Typography';
import { forwardRef, memo, useState } from 'react';
import FuseThemeSchemes from '@fuse/core/FuseThemeSchemes';
import { useSwipeable } from 'react-swipeable';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import themesConfig from 'app/configs/themesConfig';
import { changeFuseTheme } from 'app/store/fuse/settingsSlice';
import { useDispatch } from 'react-redux';
import FuseSettingsViewerDialog from './FuseSettingsViewerDialog';
const Root = styled('div')(({ theme }) => ({
position: 'absolute',
height: 80,
right: 0,
top: 160,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
padding: 0,
borderTopLeftRadius: 6,
borderBottomLeftRadius: 6,
borderBottomRightRadius: 0,
borderTopRightRadius: 0,
zIndex: 999,
color: theme.palette.getContrastText(red[500]),
backgroundColor: red[400],
'&:hover': {
backgroundColor: red[500],
},
'& .settingsButton': {
'& > span': {
animation: 'rotating 3s linear infinite',
},
},
'@keyframes rotating': {
from: {
transform: 'rotate(0deg)',
},
to: {
transform: 'rotate(360deg)',
},
},
}));
const StyledDialog = styled(Dialog)(({ theme }) => ({
'& .MuiDialog-paper': {
position: 'fixed',
width: 380,
maxWidth: '90vw',
backgroundColor: theme.palette.background.paper,
top: 0,
height: '100%',
minHeight: '100%',
bottom: 0,
right: 0,
margin: 0,
zIndex: 1000,
borderRadius: 0,
},
}));
const Transition = forwardRef(function Transition(props, ref) {
const theme = useTheme();
return <Slide direction={theme.direction === 'ltr' ? 'left' : 'right'} ref={ref} {...props} />;
});
function SettingsPanel() {
const theme = useTheme();
const [open, setOpen] = useState(false);
const dispatch = useDispatch();
const handlerOptions = {
onSwipedLeft: () => {
return open && theme.direction === 'rtl' && handleClose();
},
onSwipedRight: () => {
return open && theme.direction === 'ltr' && handleClose();
},
};
const settingsHandlers = useSwipeable(handlerOptions);
const shemesHandlers = useSwipeable(handlerOptions);
const handleOpen = (panelId) => {
setOpen(panelId);
};
const handleClose = () => {
setOpen(false);
};
return (
<>
<Root id="fuse-settings-schemes" className="buttonWrapper">
<Button
className="settingsButton min-w-40 w-40 h-40 m-0"
onClick={() => handleOpen('settings')}
variant="text"
color="inherit"
disableRipple
>
<span>
<FuseSvgIcon size={20}>heroicons-solid:cog</FuseSvgIcon>
</span>
</Button>
<Button
className="min-w-40 w-40 h-40 m-0"
onClick={() => handleOpen('schemes')}
variant="text"
color="inherit"
disableRipple
>
<FuseSvgIcon size={20}>heroicons-outline:color-swatch</FuseSvgIcon>
</Button>
</Root>
<StyledDialog
TransitionComponent={Transition}
aria-labelledby="settings-panel"
aria-describedby="settings"
open={open === 'settings'}
onClose={handleClose}
BackdropProps={{ invisible: true }}
classes={{
paper: 'shadow-lg',
}}
{...settingsHandlers}
>
<FuseScrollbars className="p-16 sm:p-32">
<IconButton
className="fixed top-0 ltr:right-0 rtl:left-0 z-10"
onClick={handleClose}
size="large"
>
<FuseSvgIcon>heroicons-outline:x</FuseSvgIcon>
</IconButton>
<Typography className="mb-32 font-semibold" variant="h6">
Theme Settings
</Typography>
<FuseSettings />
<FuseSettingsViewerDialog className="mt-32" />
</FuseScrollbars>
</StyledDialog>
<StyledDialog
TransitionComponent={Transition}
aria-labelledby="schemes-panel"
aria-describedby="schemes"
open={open === 'schemes'}
onClose={handleClose}
BackdropProps={{ invisible: true }}
classes={{
paper: 'shadow-lg',
}}
{...shemesHandlers}
>
<FuseScrollbars className="p-16 sm:p-32">
<IconButton
className="fixed top-0 ltr:right-0 rtl:left-0 z-10"
onClick={handleClose}
size="large"
>
<FuseSvgIcon>heroicons-outline:x</FuseSvgIcon>
</IconButton>
<Typography className="mb-32" variant="h6">
Theme Color Schemes
</Typography>
<Typography className="mb-24 text-12 italic text-justify" color="text.secondary">
* Selected color scheme will be applied to all theme layout elements (navbar, toolbar,
etc.). You can also select a different color scheme for each layout element at theme
settings.
</Typography>
<FuseThemeSchemes
themes={themesConfig}
onSelect={(_theme) => {
dispatch(changeFuseTheme(_theme));
}}
/>
</FuseScrollbars>
</StyledDialog>
</>
);
}
export default memo(SettingsPanel);

View File

@@ -0,0 +1,115 @@
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import MenuItem from '@mui/material/MenuItem';
import Popover from '@mui/material/Popover';
import Typography from '@mui/material/Typography';
import { useState } from 'react';
import { useSelector } from 'react-redux';
import { Link, NavLink } from 'react-router-dom';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import { selectUser } from 'app/store/userSlice';
function UserMenu(props) {
const user = useSelector(selectUser);
const [userMenu, setUserMenu] = useState(null);
const userMenuClick = (event) => {
setUserMenu(event.currentTarget);
};
const userMenuClose = () => {
setUserMenu(null);
};
return (
<>
<Button
className="min-h-40 min-w-40 px-0 md:px-16 py-0 md:py-6"
onClick={userMenuClick}
color="inherit"
>
<div className="hidden md:flex flex-col mx-4 items-end">
<Typography component="span" className="font-semibold flex">
{user.data.displayName}
</Typography>
<Typography className="text-11 font-medium capitalize" color="text.secondary">
{user.role.toString()}
{(!user.role || (Array.isArray(user.role) && user.role.length === 0)) && 'Guest'}
</Typography>
</div>
{user.data.photoURL ? (
<Avatar className="md:mx-4" alt="user photo" src={user.data.photoURL} />
) : (
<Avatar className="md:mx-4">{user.data.displayName[0]}</Avatar>
)}
</Button>
<Popover
open={Boolean(userMenu)}
anchorEl={userMenu}
onClose={userMenuClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
classes={{
paper: 'py-8',
}}
>
{!user.role || user.role.length === 0 ? (
<>
<MenuItem component={Link} to="/sign-in" role="button">
<ListItemIcon className="min-w-40">
<FuseSvgIcon>heroicons-outline:lock-closed</FuseSvgIcon>
</ListItemIcon>
<ListItemText primary="Sign In" />
</MenuItem>
<MenuItem component={Link} to="/sign-up" role="button">
<ListItemIcon className="min-w-40">
<FuseSvgIcon>heroicons-outline:user-add </FuseSvgIcon>
</ListItemIcon>
<ListItemText primary="Sign up" />
</MenuItem>
</>
) : (
<>
<MenuItem component={Link} to="/apps/profile" onClick={userMenuClose} role="button">
<ListItemIcon className="min-w-40">
<FuseSvgIcon>heroicons-outline:user-circle</FuseSvgIcon>
</ListItemIcon>
<ListItemText primary="My Profile" />
</MenuItem>
<MenuItem component={Link} to="/apps/mailbox" onClick={userMenuClose} role="button">
<ListItemIcon className="min-w-40">
<FuseSvgIcon>heroicons-outline:mail-open</FuseSvgIcon>
</ListItemIcon>
<ListItemText primary="Inbox" />
</MenuItem>
<MenuItem
component={NavLink}
to="/sign-out"
onClick={() => {
userMenuClose();
}}
>
<ListItemIcon className="min-w-40">
<FuseSvgIcon>heroicons-outline:logout</FuseSvgIcon>
</ListItemIcon>
<ListItemText primary="Sign out" />
</MenuItem>
</>
)}
</Popover>
</>
);
}
export default UserMenu;

View File

@@ -0,0 +1,56 @@
import { styled } from '@mui/material/styles';
import Avatar from '@mui/material/Avatar';
import Typography from '@mui/material/Typography';
import { useSelector } from 'react-redux';
import { selectUser } from 'app/store/userSlice';
const Root = styled('div')(({ theme }) => ({
'& .username, & .email': {
transition: theme.transitions.create('opacity', {
duration: theme.transitions.duration.shortest,
easing: theme.transitions.easing.easeInOut,
}),
},
'& .avatar': {
background: theme.palette.background.default,
transition: theme.transitions.create('all', {
duration: theme.transitions.duration.shortest,
easing: theme.transitions.easing.easeInOut,
}),
bottom: 0,
'& > img': {
borderRadius: '50%',
},
},
}));
function UserNavbarHeader(props) {
const user = useSelector(selectUser);
return (
<Root className="user relative flex flex-col items-center justify-center p-16 pb-14 shadow-0">
<div className="flex items-center justify-center mb-24">
<Avatar
sx={{
backgroundColor: 'background.paper',
color: 'text.secondary',
}}
className="avatar text-32 font-bold w-96 h-96"
src={user.data.photoURL}
alt={user.data.displayName}
>
{user.data.displayName.charAt(0)}
</Avatar>
</div>
<Typography className="username text-14 whitespace-nowrap font-medium">
{user.data.displayName}
</Typography>
<Typography className="email text-13 whitespace-nowrap font-medium" color="text.secondary">
{user.data.email}
</Typography>
</Root>
);
}
export default UserNavbarHeader;

View File

@@ -0,0 +1,229 @@
import { styled } from '@mui/material/styles';
import IconButton from '@mui/material/IconButton';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import clsx from 'clsx';
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import InputBase from '@mui/material/InputBase';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import { selectSelectedContactId } from './store/contactsSlice';
import { selectChat, sendMessage } from './store/chatSlice';
import { selectUser } from './store/userSlice';
const StyledMessageRow = styled('div')(({ theme }) => ({
'&.contact': {
'& .bubble': {
backgroundColor: theme.palette.secondary.light,
color: theme.palette.secondary.contrastText,
borderTopLeftRadius: 5,
borderBottomLeftRadius: 5,
borderTopRightRadius: 20,
borderBottomRightRadius: 20,
'& .time': {
marginLeft: 12,
},
},
'&.first-of-group': {
'& .bubble': {
borderTopLeftRadius: 20,
},
},
'&.last-of-group': {
'& .bubble': {
borderBottomLeftRadius: 20,
},
},
},
'&.me': {
paddingLeft: 40,
'& .bubble': {
marginLeft: 'auto',
backgroundColor: theme.palette.primary.light,
color: theme.palette.primary.contrastText,
borderTopLeftRadius: 20,
borderBottomLeftRadius: 20,
borderTopRightRadius: 5,
borderBottomRightRadius: 5,
'& .time': {
justifyContent: 'flex-end',
right: 0,
marginRight: 12,
},
},
'&.first-of-group': {
'& .bubble': {
borderTopRightRadius: 20,
},
},
'&.last-of-group': {
'& .bubble': {
borderBottomRightRadius: 20,
},
},
},
'&.contact + .me, &.me + .contact': {
paddingTop: 20,
marginTop: 20,
},
'&.first-of-group': {
'& .bubble': {
borderTopLeftRadius: 20,
paddingTop: 13,
},
},
'&.last-of-group': {
'& .bubble': {
borderBottomLeftRadius: 20,
paddingBottom: 13,
'& .time': {
display: 'flex',
},
},
},
}));
function Chat(props) {
const dispatch = useDispatch();
const selectedContactId = useSelector(selectSelectedContactId);
const chat = useSelector(selectChat);
const user = useSelector(selectUser);
const chatScroll = useRef(null);
const [messageText, setMessageText] = useState('');
useEffect(() => {
scrollToBottom();
}, [chat]);
function scrollToBottom() {
if (!chatScroll.current) {
return;
}
chatScroll.current.scrollTo({
top: chatScroll.current.scrollHeight,
behavior: 'smooth',
});
}
const onInputChange = (ev) => {
setMessageText(ev.target.value);
};
return (
<Paper
className={clsx('flex flex-col relative pb-64 shadow', props.className)}
sx={{ background: (theme) => theme.palette.background.default }}
>
<div ref={chatScroll} className="flex flex-1 flex-col overflow-y-auto overscroll-contain">
<div className="flex flex-col pt-16">
{useMemo(() => {
function isFirstMessageOfGroup(item, i) {
return i === 0 || (chat[i - 1] && chat[i - 1].contactId !== item.contactId);
}
function isLastMessageOfGroup(item, i) {
return (
i === chat.length - 1 || (chat[i + 1] && chat[i + 1].contactId !== item.contactId)
);
}
return chat?.length > 0
? chat.map((item, i) => {
return (
<StyledMessageRow
key={i}
className={clsx(
'flex flex-col grow-0 shrink-0 items-start justify-end relative px-16 pb-4',
item.contactId === user.id ? 'me' : 'contact',
{ 'first-of-group': isFirstMessageOfGroup(item, i) },
{ 'last-of-group': isLastMessageOfGroup(item, i) },
i + 1 === chat.length && 'pb-72'
)}
>
<div className="bubble flex relative items-center justify-center p-12 max-w-full">
<div className="leading-tight whitespace-pre-wrap">{item.value}</div>
<Typography
className="time absolute hidden w-full text-11 mt-8 -mb-24 ltr:left-0 rtl:right-0 bottom-0 whitespace-nowrap"
color="text.secondary"
>
{formatDistanceToNow(new Date(item.createdAt), { addSuffix: true })}
</Typography>
</div>
</StyledMessageRow>
);
})
: null;
}, [chat, user?.id])}
</div>
{chat?.length === 0 && (
<div className="flex flex-col flex-1">
<div className="flex flex-col flex-1 items-center justify-center">
<FuseSvgIcon size={128} color="disabled">
heroicons-outline:chat
</FuseSvgIcon>
</div>
<Typography className="px-16 pb-24 text-center" color="text.secondary">
Start a conversation by typing your message below.
</Typography>
</div>
)}
</div>
{useMemo(() => {
const onMessageSubmit = (ev) => {
ev.preventDefault();
if (messageText === '') {
return;
}
dispatch(
sendMessage({
messageText,
chatId: chat.id,
contactId: selectedContactId,
})
).then(() => {
setMessageText('');
});
};
return (
<>
{chat && (
<form
onSubmit={onMessageSubmit}
className="pb-16 px-8 absolute bottom-0 left-0 right-0"
>
<Paper className="rounded-24 flex items-center relative shadow">
<InputBase
autoFocus={false}
id="message-input"
className="flex flex-1 grow shrink-0 mx-16 ltr:mr-48 rtl:ml-48 my-8"
placeholder="Type your message"
onChange={onInputChange}
value={messageText}
/>
<IconButton
className="absolute ltr:right-0 rtl:left-0 top-0"
type="submit"
size="large"
>
<FuseSvgIcon className="rotate-90" color="action">
heroicons-outline:paper-airplane
</FuseSvgIcon>
</IconButton>
</Paper>
</form>
)}
</>
);
}, [chat, dispatch, messageText, selectedContactId])}
</Paper>
);
}
export default Chat;

View File

@@ -0,0 +1,227 @@
import AppBar from '@mui/material/AppBar';
import { styled, useTheme } from '@mui/material/styles';
import Avatar from '@mui/material/Avatar';
import IconButton from '@mui/material/IconButton';
import Paper from '@mui/material/Paper';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import withReducer from 'app/store/withReducer';
import keycode from 'keycode';
import { memo, useCallback, useEffect, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSwipeable } from 'react-swipeable';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import Chat from './Chat';
import ContactList from './ContactList';
import reducer from './store';
import { getContacts, selectContacts, selectSelectedContactId } from './store/contactsSlice';
import { closeChatPanel, openChatPanel, selectChatPanelState } from './store/stateSlice';
import { getUserData } from './store/userSlice';
import { getChats } from './store/chatsSlice';
const Root = styled('div')(({ theme, opened }) => ({
position: 'sticky',
display: 'flex',
top: 0,
width: 70,
maxWidth: 70,
minWidth: 70,
height: '100vh',
zIndex: 1000,
[theme.breakpoints.down('lg')]: {
position: 'fixed',
height: '100%',
width: 0,
maxWidth: 0,
minWidth: 0,
},
...(opened && {
overflow: 'visible',
}),
...(!opened && {
overflow: 'hidden',
animation: `hide-panel 1ms linear ${theme.transitions.duration.standard}`,
animationFillMode: 'forwards',
}),
'& > .panel': {
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
width: 360,
minWidth: 360,
height: '100%',
margin: 0,
overflow: 'hidden',
zIndex: 1000,
backgroundColor: theme.palette.background.paper,
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
transform: 'translate3d(0,0,0)',
transition: theme.transitions.create(['transform'], {
easing: theme.transitions.easing.easeInOut,
duration: theme.transitions.duration.standard,
}),
...(opened && {
transform: theme.direction === 'rtl' ? 'translate3d(290px,0,0)' : 'translate3d(-290px,0,0)',
}),
[theme.breakpoints.down('lg')]: {
left: 'auto',
position: 'fixed',
transform: theme.direction === 'rtl' ? 'translate3d(-360px,0,0)' : 'translate3d(360px,0,0)',
boxShadow: 'none',
width: 320,
minWidth: 320,
maxWidth: '100%',
...(opened && {
transform: 'translate3d(0,0,0)',
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
}),
},
},
'@keyframes hide-panel': {
'0%': {
overflow: 'visible',
},
'99%': {
overflow: 'visible',
},
'100%': {
overflow: 'hidden',
},
},
}));
function ChatPanel(props) {
const dispatch = useDispatch();
const contacts = useSelector(selectContacts);
const selectedContactId = useSelector(selectSelectedContactId);
const state = useSelector(selectChatPanelState);
const theme = useTheme();
const ref = useRef();
const handlers = useSwipeable({
onSwipedLeft: () => {
return state && theme.direction === 'rtl' && dispatch(closeChatPanel());
},
onSwipedRight: () => {
return state && theme.direction === 'ltr' && dispatch(closeChatPanel());
},
});
const selectedContact = contacts.find((_contact) => _contact.id === selectedContactId);
const handleDocumentKeyDown = useCallback(
(event) => {
if (keycode(event) === 'esc') {
dispatch(closeChatPanel());
}
},
[dispatch]
);
useEffect(() => {
dispatch(getUserData());
dispatch(getContacts());
dispatch(getChats());
return () => {
document.removeEventListener('keydown', handleDocumentKeyDown);
};
}, [dispatch, handleDocumentKeyDown]);
useEffect(() => {
if (state) {
document.addEventListener('keydown', handleDocumentKeyDown);
} else {
document.removeEventListener('keydown', handleDocumentKeyDown);
}
}, [handleDocumentKeyDown, state]);
/**
* Click Away Listener
*/
useEffect(() => {
function handleDocumentClick(ev) {
if (ref.current && !ref.current.contains(ev.target)) {
dispatch(closeChatPanel());
}
}
if (state) {
document.addEventListener('click', handleDocumentClick, true);
} else {
document.removeEventListener('click', handleDocumentClick, true);
}
return () => {
document.removeEventListener('click', handleDocumentClick, true);
};
}, [state, dispatch]);
return (
<Root opened={state ? 1 : 0} {...handlers}>
<div className="panel flex flex-col max-w-full" ref={ref}>
<AppBar position="static" className="shadow-md">
<Toolbar className="px-4">
{(!state || !selectedContactId) && (
<div className="flex flex-1 items-center px-8 space-x-12">
<IconButton
className=""
color="inherit"
onClick={(ev) => dispatch(openChatPanel())}
size="large"
>
<FuseSvgIcon size={24}>heroicons-outline:chat-alt-2</FuseSvgIcon>
</IconButton>
{!selectedContactId && (
<Typography className="text-16" color="inherit">
Team Chat
</Typography>
)}
</div>
)}
{state && selectedContact && (
<div className="flex flex-1 items-center px-12">
<Avatar src={selectedContact.avatar} />
<Typography className="mx-16 text-16" color="inherit">
{selectedContact.name}
</Typography>
</div>
)}
<div className="flex px-4">
<IconButton onClick={(ev) => dispatch(closeChatPanel())} color="inherit" size="large">
<FuseSvgIcon>heroicons-outline:x</FuseSvgIcon>
</IconButton>
</div>
</Toolbar>
</AppBar>
<Paper className="flex flex-1 flex-row min-h-px shadow-0">
<ContactList className="flex shrink-0" />
{state && selectedContact ? (
<Chat className="flex flex-1 z-10" />
) : (
<div className="flex flex-col flex-1 items-center justify-center p-24">
<FuseSvgIcon size={128} color="disabled">
heroicons-outline:chat
</FuseSvgIcon>
<Typography className="px-16 pb-24 mt-24 text-center" color="text.secondary">
Select a contact to start a conversation.
</Typography>
</div>
)}
</Paper>
</div>
</Root>
);
}
export default withReducer('chatPanel', reducer)(memo(ChatPanel));

View File

@@ -0,0 +1,20 @@
import IconButton from '@mui/material/IconButton';
import { useDispatch } from 'react-redux';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import { toggleChatPanel } from './store/stateSlice';
const ChatPanelToggleButton = (props) => {
const dispatch = useDispatch();
return (
<IconButton className="w-40 h-40" onClick={(ev) => dispatch(toggleChatPanel())} size="large">
{props.children}
</IconButton>
);
};
ChatPanelToggleButton.defaultProps = {
children: <FuseSvgIcon>heroicons-outline:chat</FuseSvgIcon>,
};
export default ChatPanelToggleButton;

View File

@@ -0,0 +1,94 @@
import Tooltip from '@mui/material/Tooltip';
import Button from '@mui/material/Button';
import clsx from 'clsx';
import Avatar from '@mui/material/Avatar';
import { styled } from '@mui/material/styles';
const Root = styled(Tooltip)(({ theme, active }) => ({
width: 70,
minWidth: 70,
flex: '0 0 auto',
...(active && {
'&:after': {
position: 'absolute',
top: 8,
right: 0,
bottom: 8,
content: "''",
width: 4,
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
backgroundColor: theme.palette.primary.main,
},
}),
}));
const StyledUreadBadge = styled('div')(({ theme, value }) => ({
position: 'absolute',
minWidth: 18,
height: 18,
top: 4,
left: 10,
borderRadius: 9,
padding: '0 5px',
fontSize: 11,
textAlign: 'center',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: theme.palette.secondary.main,
color: theme.palette.secondary.contrastText,
boxShadow: '0 2px 2px 0 rgba(0, 0, 0, 0.35)',
zIndex: 10,
}));
const StyledStatus = styled('div')(({ theme, value }) => ({
position: 'absolute',
width: 12,
height: 12,
bottom: 4,
left: 44,
border: `2px solid ${theme.palette.background.default}`,
borderRadius: '50%',
zIndex: 10,
...(value === 'online' && {
backgroundColor: '#4CAF50',
}),
...(value === 'do-not-disturb' && {
backgroundColor: '#F44336',
}),
...(value === 'away' && {
backgroundColor: '#FFC107',
}),
...(value === 'offline' && {
backgroundColor: '#646464',
}),
}));
const ContactButton = ({ contact, selectedContactId, onClick }) => {
return (
<Root title={contact.name} placement="left" active={selectedContactId === contact.id ? 1 : 0}>
<Button
onClick={() => onClick(contact.id)}
className={clsx(
'contactButton rounded-0 py-4 h-auto min-h-auto max-h-none',
selectedContactId === contact.id && 'active'
)}
>
{contact.unread && <StyledUreadBadge>{contact.unread}</StyledUreadBadge>}
<StyledStatus value={contact.status} />
<Avatar src={contact.avatar} alt={contact.name}>
{!contact.avatar || contact.avatar === '' ? contact.name[0] : ''}
</Avatar>
</Button>
</Root>
);
};
export default ContactButton;

View File

@@ -0,0 +1,106 @@
import FuseScrollbars from '@fuse/core/FuseScrollbars';
import { styled } from '@mui/material/styles';
import Divider from '@mui/material/Divider';
import { motion } from 'framer-motion';
import { memo, useMemo, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getChat } from './store/chatSlice';
import { selectContacts, selectSelectedContactId } from './store/contactsSlice';
import { openChatPanel } from './store/stateSlice';
import ContactButton from './ContactButton';
import { selectChats } from './store/chatsSlice';
const Root = styled(FuseScrollbars)(({ theme }) => ({
background: theme.palette.background.paper,
}));
function ContactList(props) {
const dispatch = useDispatch();
const contacts = useSelector(selectContacts);
const selectedContactId = useSelector(selectSelectedContactId);
const chats = useSelector(selectChats);
const contactListScroll = useRef(null);
const scrollToTop = () => {
contactListScroll.current.scrollTop = 0;
};
return (
<Root
className="flex shrink-0 flex-col overflow-y-auto py-8 overscroll-contain"
ref={contactListScroll}
option={{ suppressScrollX: true, wheelPropagation: false }}
>
{useMemo(() => {
const chatListContacts =
contacts.length > 0 && chats.length > 0
? chats.map((_chat) => ({
..._chat,
...contacts.find((_contact) => _contact.id === _chat.contactId),
}))
: [];
const handleContactClick = (contactId) => {
dispatch(openChatPanel());
dispatch(getChat(contactId));
scrollToTop();
};
const container = {
show: {
transition: {
staggerChildren: 0.05,
},
},
};
const item = {
hidden: { opacity: 0, scale: 0.6 },
show: { opacity: 1, scale: 1 },
};
return (
contacts.length > 0 && (
<>
<motion.div
variants={container}
initial="hidden"
animate="show"
className="flex flex-col shrink-0"
>
{chatListContacts &&
chatListContacts.map((contact) => {
return (
<motion.div variants={item} key={contact.id}>
<ContactButton
contact={contact}
selectedContactId={selectedContactId}
onClick={handleContactClick}
/>
</motion.div>
);
})}
<Divider className="mx-24 my-8" />
{contacts.map((contact) => {
const chatContact = chats.find((_chat) => _chat.contactId === contact.id);
return !chatContact ? (
<motion.div variants={item} key={contact.id}>
<ContactButton
contact={contact}
selectedContactId={selectedContactId}
onClick={handleContactClick}
/>
</motion.div>
) : null;
})}
</motion.div>
</>
)
);
}, [chats, contacts, dispatch, selectedContactId])}
</Root>
);
}
export default memo(ContactList);

View File

@@ -0,0 +1,50 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import axios from 'axios';
import { setSelectedContactId } from './contactsSlice';
import { closeChatPanel } from './stateSlice';
import { getChats } from './chatsSlice';
export const getChat = createAsyncThunk(
'chatPanel/chat/getChat',
async (contactId, { dispatch, getState }) => {
const response = await axios.get(`/api/chat/chats/${contactId}`);
const data = await response.data;
dispatch(setSelectedContactId(contactId));
return data;
}
);
export const sendMessage = createAsyncThunk(
'chatPanel/chat/sendMessage',
async ({ messageText, chatId, contactId }, { dispatch, getState }) => {
const response = await axios.post(`/api/chat/chats/${contactId}`, messageText);
const data = await response.data;
dispatch(getChats());
return data;
}
);
const chatSlice = createSlice({
name: 'chatPanel/chat',
initialState: [],
reducers: {
removeChat: (state, action) => null,
},
extraReducers: {
[getChat.fulfilled]: (state, action) => action.payload,
[sendMessage.fulfilled]: (state, action) => [...state, action.payload],
[closeChatPanel]: (state, action) => null,
},
});
export const { removeChat } = chatSlice.actions;
export const selectChat = ({ chatPanel }) => chatPanel.chat;
export default chatSlice.reducer;

View File

@@ -0,0 +1,26 @@
import { createAsyncThunk, createEntityAdapter, createSlice } from '@reduxjs/toolkit';
import axios from 'axios';
export const getChats = createAsyncThunk('chatPanel/chats/getChats', async (params) => {
const response = await axios.get('/api/chat/chats', { params });
const data = await response.data;
return data;
});
const chatsAdapter = createEntityAdapter({});
export const { selectAll: selectChats, selectById: selectChatById } = chatsAdapter.getSelectors(
(state) => state.chatPanel.chats
);
const chatsSlice = createSlice({
name: 'chatPanel/chats',
initialState: chatsAdapter.getInitialState(),
extraReducers: {
[getChats.fulfilled]: chatsAdapter.setAll,
},
});
export default chatsSlice.reducer;

View File

@@ -0,0 +1,44 @@
import { createAsyncThunk, createEntityAdapter, createSlice } from '@reduxjs/toolkit';
import axios from 'axios';
import { closeChatPanel } from './stateSlice';
export const getContacts = createAsyncThunk('chatPanel/contacts/getContacts', async (params) => {
const response = await axios.get('/api/chat/contacts', { params });
const data = await response.data;
return data;
});
const contactsAdapter = createEntityAdapter({});
export const { selectAll: selectContacts, selectById: selectContactById } =
contactsAdapter.getSelectors((state) => state.chatPanel.contacts);
const contactsSlice = createSlice({
name: 'chatPanel/contacts',
initialState: contactsAdapter.getInitialState({
selectedContactId: null,
}),
reducers: {
setSelectedContactId: (state, action) => {
state.selectedContactId = action.payload;
},
removeSelectedContactId: (state, action) => {
state.selectedContactId = null;
},
},
extraReducers: {
[getContacts.fulfilled]: contactsAdapter.setAll,
[closeChatPanel]: (state, action) => {
state.selectedContactId = null;
},
},
});
export const { setSelectedContactId, removeSelectedContactId } = contactsSlice.actions;
export const selectSelectedContactId = ({ chatPanel }) => chatPanel.contacts.selectedContactId;
export default contactsSlice.reducer;

View File

@@ -0,0 +1,16 @@
import { combineReducers } from '@reduxjs/toolkit';
import chat from './chatSlice';
import chats from './chatsSlice';
import contacts from './contactsSlice';
import state from './stateSlice';
import user from './userSlice';
const reducer = combineReducers({
user,
contacts,
chat,
chats,
state,
});
export default reducer;

View File

@@ -0,0 +1,18 @@
import { createSlice } from '@reduxjs/toolkit';
const stateSlice = createSlice({
name: 'chatPanel/state',
initialState: false,
reducers: {
toggleChatPanel: (state, action) => !state,
openChatPanel: (state, action) => true,
closeChatPanel: (state, action) => false,
},
extraReducers: {},
});
export const { toggleChatPanel, openChatPanel, closeChatPanel } = stateSlice.actions;
export const selectChatPanelState = ({ chatPanel }) => chatPanel.state;
export default stateSlice.reducer;

View File

@@ -0,0 +1,33 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import axios from 'axios';
export const getUserData = createAsyncThunk('chatPanel/user/getUserData', async () => {
const response = await axios.get('/api/chat/user');
const data = await response.data;
return data;
});
export const updateUserData = createAsyncThunk('chatPanel/user/updateUserData', async (newData) => {
const response = await axios.post('/api/chat/user', newData);
const data = await response.data;
return data;
});
const userSlice = createSlice({
name: 'chatPanel/user',
initialState: null,
extraReducers: {
[getUserData.fulfilled]: (state, action) => action.payload,
[updateUserData.fulfilled]: (state, action) => action.payload,
},
});
export const { updateUserChatList } = userSlice.actions;
export const selectUser = ({ chatPanel }) => chatPanel.user;
export default userSlice.reducer;

View File

@@ -0,0 +1,87 @@
import Card from '@mui/material/Card';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import clsx from 'clsx';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import NavLinkAdapter from '@fuse/core/NavLinkAdapter';
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
function NotificationCard(props) {
const { item, className } = props;
const variant = item?.variant || '';
const handleClose = (ev) => {
ev.preventDefault();
ev.stopPropagation();
if (props.onClose) {
props.onClose(item.id);
}
};
return (
<Card
className={clsx(
'flex items-center relative w-full rounded-16 p-20 min-h-64 shadow space-x-8',
variant === 'success' && 'bg-green-600 text-white',
variant === 'info' && 'bg-blue-700 text-white',
variant === 'error' && 'bg-red-600 text-white',
variant === 'warning' && 'bg-orange-600 text-white',
className
)}
elevation={0}
component={item.useRouter ? NavLinkAdapter : 'div'}
to={item.link || ''}
role={item.link && 'button'}
>
{item.icon && !item.image && (
<Box
sx={{ backgroundColor: 'background.default' }}
className="flex shrink-0 items-center justify-center w-32 h-32 mr-12 rounded-full"
>
<FuseSvgIcon className="opacity-75" color="inherit">
{item.icon}
</FuseSvgIcon>
</Box>
)}
{item.image && (
<img
className="shrink-0 w-32 h-32 mr-12 rounded-full overflow-hidden object-cover object-center"
src={item.image}
alt="Notification"
/>
)}
<div className="flex flex-col flex-auto">
{item.title && <Typography className="font-semibold line-clamp-1">{item.title}</Typography>}
{item.description && (
<div className="line-clamp-2" dangerouslySetInnerHTML={{ __html: item.description }} />
)}
{item.item && (
<Typography className="mt-8 text-sm leading-none " color="text.secondary">
{formatDistanceToNow(new Date(item.time), { addSuffix: true })}
</Typography>
)}
</div>
<IconButton
disableRipple
className="top-0 right-0 absolute p-8"
color="inherit"
size="small"
onClick={handleClose}
>
<FuseSvgIcon size={12} className="opacity-75" color="inherit">
heroicons-solid:x
</FuseSvgIcon>
</IconButton>
{item.children}
</Card>
);
}
export default NotificationCard;

View File

@@ -0,0 +1,39 @@
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
const NotificationIcon = ({ value }) => {
switch (value) {
case 'error': {
return (
<FuseSvgIcon className="mr-8 opacity-75" color="inherit">
heroicons-outline:minus-circle
</FuseSvgIcon>
);
}
case 'success': {
return (
<FuseSvgIcon className="mr-8 opacity-75" color="inherit">
heroicons-outline:check-circle
</FuseSvgIcon>
);
}
case 'warning': {
return (
<FuseSvgIcon className="mr-8 opacity-75" color="inherit">
heroicons-outline:exclamation-circle
</FuseSvgIcon>
);
}
case 'info': {
return (
<FuseSvgIcon className="mr-8 opacity-75" color="inherit">
heroicons-outline:information-circle
</FuseSvgIcon>
);
}
default: {
return null;
}
}
};
export default NotificationIcon;

View File

@@ -0,0 +1,139 @@
import FuseScrollbars from '@fuse/core/FuseScrollbars';
import { styled } from '@mui/material/styles';
import IconButton from '@mui/material/IconButton';
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
import Typography from '@mui/material/Typography';
import withReducer from 'app/store/withReducer';
import { useSnackbar } from 'notistack';
import { memo, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import Button from '@mui/material/Button';
import NotificationTemplate from 'app/theme-layouts/shared-components/notificationPanel/NotificationTemplate';
import NotificationModel from './model/NotificationModel';
import NotificationCard from './NotificationCard';
import {
addNotification,
dismissAll,
dismissItem,
getNotifications,
selectNotifications,
} from './store/dataSlice';
import reducer from './store';
import {
closeNotificationPanel,
selectNotificationPanelState,
toggleNotificationPanel,
} from './store/stateSlice';
const StyledSwipeableDrawer = styled(SwipeableDrawer)(({ theme }) => ({
'& .MuiDrawer-paper': {
backgroundColor: theme.palette.background.default,
width: 320,
},
}));
function NotificationPanel(props) {
const location = useLocation();
const dispatch = useDispatch();
const state = useSelector(selectNotificationPanelState);
const notifications = useSelector(selectNotifications);
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
useEffect(() => {
/*
Get Notifications from db
*/
dispatch(getNotifications());
}, [dispatch]);
useEffect(() => {
if (state) {
dispatch(closeNotificationPanel());
}
// eslint-disable-next-line
}, [location, dispatch]);
function handleClose() {
dispatch(closeNotificationPanel());
}
function handleDismiss(id) {
dispatch(dismissItem(id));
}
function handleDismissAll() {
dispatch(dismissAll());
}
function demoNotification() {
const item = NotificationModel({ title: 'Great Job! this is awesome.' });
enqueueSnackbar(item.title, {
key: item.id,
// autoHideDuration: 3000,
content: () => (
<NotificationTemplate
item={item}
onClose={() => {
closeSnackbar(item.id);
}}
/>
),
});
dispatch(addNotification(item));
}
return (
<StyledSwipeableDrawer
open={state}
anchor="right"
onOpen={(ev) => {}}
onClose={(ev) => dispatch(toggleNotificationPanel())}
disableSwipeToOpen
>
<IconButton className="m-4 absolute top-0 right-0 z-999" onClick={handleClose} size="large">
<FuseSvgIcon color="action">heroicons-outline:x</FuseSvgIcon>
</IconButton>
{notifications.length > 0 ? (
<FuseScrollbars className="p-16">
<div className="flex flex-col">
<div className="flex justify-between items-end pt-136 mb-36">
<Typography className="text-28 font-semibold leading-none">Notifications</Typography>
<Typography
className="text-12 underline cursor-pointer"
color="secondary"
onClick={handleDismissAll}
>
dismiss all
</Typography>
</div>
{notifications.map((item) => (
<NotificationCard
key={item.id}
className="mb-16"
item={item}
onClose={handleDismiss}
/>
))}
</div>
</FuseScrollbars>
) : (
<div className="flex flex-1 items-center justify-center p-16">
<Typography className="text-24 text-center" color="text.secondary">
There are no notifications for now.
</Typography>
</div>
)}
<div className="flex items-center justify-center py-16">
<Button size="small" variant="outlined" onClick={demoNotification}>
Create a notification example
</Button>
</div>
</StyledSwipeableDrawer>
);
}
export default withReducer('notificationPanel', reducer)(memo(NotificationPanel));

View File

@@ -0,0 +1,32 @@
import Badge from '@mui/material/Badge';
import IconButton from '@mui/material/IconButton';
import { useDispatch, useSelector } from 'react-redux';
import withReducer from 'app/store/withReducer';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import reducer from './store';
import { selectNotifications } from './store/dataSlice';
import { toggleNotificationPanel } from './store/stateSlice';
function NotificationPanelToggleButton(props) {
const notifications = useSelector(selectNotifications);
const dispatch = useDispatch();
return (
<IconButton
className="w-40 h-40"
onClick={(ev) => dispatch(toggleNotificationPanel())}
size="large"
>
<Badge color="secondary" variant="dot" invisible={notifications.length === 0}>
{props.children}
</Badge>
</IconButton>
);
}
NotificationPanelToggleButton.defaultProps = {
children: <FuseSvgIcon>heroicons-outline:bell</FuseSvgIcon>,
};
export default withReducer('notificationPanel', reducer)(NotificationPanelToggleButton);

View File

@@ -0,0 +1,18 @@
import { forwardRef } from 'react';
import { SnackbarContent } from 'notistack';
import NotificationCard from './NotificationCard';
const NotificationTemplate = forwardRef((props, ref) => {
const { item } = props;
return (
<SnackbarContent
ref={ref}
className="mx-auto max-w-320 w-full relative pointer-events-auto py-4"
>
<NotificationCard item={item} onClose={props.onClose} />
</SnackbarContent>
);
});
export default NotificationTemplate;

View File

@@ -0,0 +1,18 @@
import _ from '@lodash';
import FuseUtils from '@fuse/utils';
function NotificationModel(data) {
data = data || {};
return _.defaults(data, {
id: FuseUtils.generateGUID(),
icon: 'heroicons-solid:star',
title: '',
description: '',
time: new Date().toISOString(),
read: false,
variant: 'default',
});
}
export default NotificationModel;

View File

@@ -0,0 +1,57 @@
import { createAsyncThunk, createEntityAdapter, createSlice } from '@reduxjs/toolkit';
import axios from 'axios';
export const getNotifications = createAsyncThunk('notificationPanel/getData', async () => {
const response = await axios.get('/api/notifications');
const data = await response.data;
return data;
});
export const dismissAll = createAsyncThunk('notificationPanel/dismissAll', async () => {
const response = await axios.delete('/api/notifications');
await response.data;
return true;
});
export const dismissItem = createAsyncThunk('notificationPanel/dismissItem', async (id) => {
const response = await axios.delete(`/api/notifications/${id}`);
await response.data;
return id;
});
export const addNotification = createAsyncThunk(
'notificationPanel/addNotification',
async (item) => {
const response = await axios.post(`/api/notifications`, { ...item });
const data = await response.data;
return data;
}
);
const notificationsAdapter = createEntityAdapter({});
const initialState = notificationsAdapter.upsertMany(notificationsAdapter.getInitialState(), []);
export const { selectAll: selectNotifications, selectById: selectNotificationsById } =
notificationsAdapter.getSelectors((state) => state.notificationPanel.data);
const dataSlice = createSlice({
name: 'notificationPanel/data',
initialState,
reducers: {},
extraReducers: {
[dismissItem.fulfilled]: (state, action) =>
notificationsAdapter.removeOne(state, action.payload),
[dismissAll.fulfilled]: (state, action) => notificationsAdapter.removeAll(state),
[getNotifications.fulfilled]: (state, action) =>
notificationsAdapter.addMany(state, action.payload),
[addNotification.fulfilled]: (state, action) =>
notificationsAdapter.addOne(state, action.payload),
},
});
export default dataSlice.reducer;

View File

@@ -0,0 +1,9 @@
import { combineReducers } from '@reduxjs/toolkit';
import data from './dataSlice';
import state from './stateSlice';
const reducer = combineReducers({
data,
state,
});
export default reducer;

View File

@@ -0,0 +1,18 @@
import { createSlice } from '@reduxjs/toolkit';
const stateSlice = createSlice({
name: 'notificationPanel/state',
initialState: false,
reducers: {
toggleNotificationPanel: (state, action) => !state,
openNotificationPanel: (state, action) => true,
closeNotificationPanel: (state, action) => false,
},
});
export const { toggleNotificationPanel, openNotificationPanel, closeNotificationPanel } =
stateSlice.actions;
export const selectNotificationPanelState = ({ notificationPanel }) => notificationPanel.state;
export default stateSlice.reducer;

View File

@@ -0,0 +1,144 @@
import FuseScrollbars from '@fuse/core/FuseScrollbars';
import { styled } from '@mui/material/styles';
import Divider from '@mui/material/Divider';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
import Switch from '@mui/material/Switch';
import Typography from '@mui/material/Typography';
import withReducer from 'app/store/withReducer';
import format from 'date-fns/format';
import { memo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import { selectQuickPanelData } from './store/dataSlice';
import reducer from './store';
import { selectQuickPanelState, toggleQuickPanel } from './store/stateSlice';
const StyledSwipeableDrawer = styled(SwipeableDrawer)(({ theme }) => ({
'& .MuiDrawer-paper': {
width: 280,
},
}));
function QuickPanel(props) {
const dispatch = useDispatch();
const data = useSelector(selectQuickPanelData);
const state = useSelector(selectQuickPanelState);
const [checked, setChecked] = useState('notifications');
const handleToggle = (value) => () => {
const currentIndex = checked.indexOf(value);
const newChecked = [...checked];
if (currentIndex === -1) {
newChecked.push(value);
} else {
newChecked.splice(currentIndex, 1);
}
setChecked(newChecked);
};
return (
<StyledSwipeableDrawer
open={state}
anchor="right"
onOpen={(ev) => {}}
onClose={(ev) => dispatch(toggleQuickPanel())}
disableSwipeToOpen
>
<FuseScrollbars>
<ListSubheader component="div">Today</ListSubheader>
<div className="mb-0 py-16 px-24">
<Typography className="mb-12 text-32" color="text.secondary">
{format(new Date(), 'eeee')}
</Typography>
<div className="flex">
<Typography className="leading-none text-32" color="text.secondary">
{format(new Date(), 'dd')}
</Typography>
<Typography className="leading-none text-16" color="text.secondary">
th
</Typography>
<Typography className="leading-none text-32" color="text.secondary">
{format(new Date(), 'MMMM')}
</Typography>
</div>
</div>
<Divider />
<List>
<ListSubheader component="div">Events</ListSubheader>
{data &&
data.events.map((event) => (
<ListItem key={event.id}>
<ListItemText primary={event.title} secondary={event.detail} />
</ListItem>
))}
</List>
<Divider />
<List>
<ListSubheader component="div">Notes</ListSubheader>
{data &&
data.notes.map((note) => (
<ListItem key={note.id}>
<ListItemText primary={note.title} secondary={note.detail} />
</ListItem>
))}
</List>
<Divider />
<List>
<ListSubheader component="div">Quick Settings</ListSubheader>
<ListItem>
<ListItemIcon className="min-w-40">
<FuseSvgIcon>material-outline:notifications</FuseSvgIcon>
</ListItemIcon>
<ListItemText primary="Notifications" />
<ListItemSecondaryAction>
<Switch
color="primary"
onChange={handleToggle('notifications')}
checked={checked.indexOf('notifications') !== -1}
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemIcon className="min-w-40">
<FuseSvgIcon>material-outline:cloud</FuseSvgIcon>
</ListItemIcon>
<ListItemText primary="Cloud Sync" />
<ListItemSecondaryAction>
<Switch
color="secondary"
onChange={handleToggle('cloudSync')}
checked={checked.indexOf('cloudSync') !== -1}
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemIcon className="min-w-40">
<FuseSvgIcon>material-outline:brightness_high</FuseSvgIcon>
</ListItemIcon>
<ListItemText primary="Retro Thrusters" />
<ListItemSecondaryAction>
<Switch
color="primary"
onChange={handleToggle('retroThrusters')}
checked={checked.indexOf('retroThrusters') !== -1}
/>
</ListItemSecondaryAction>
</ListItem>
</List>
</FuseScrollbars>
</StyledSwipeableDrawer>
);
}
export default withReducer('quickPanel', reducer)(memo(QuickPanel));

View File

@@ -0,0 +1,20 @@
import IconButton from '@mui/material/IconButton';
import { useDispatch } from 'react-redux';
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import { toggleQuickPanel } from './store/stateSlice';
function QuickPanelToggleButton(props) {
const dispatch = useDispatch();
return (
<IconButton className="w-40 h-40" onClick={(ev) => dispatch(toggleQuickPanel())} size="large">
{props.children}
</IconButton>
);
}
QuickPanelToggleButton.defaultProps = {
children: <FuseSvgIcon>heroicons-outline:bookmark</FuseSvgIcon>,
};
export default QuickPanelToggleButton;

View File

@@ -0,0 +1,47 @@
import { createSlice } from '@reduxjs/toolkit';
const dataSlice = createSlice({
name: 'quickPanel/data',
initialState: {
notes: [
{
id: 1,
title: 'Best songs to listen while working',
detail: 'Last edit: May 8th, 2015',
},
{
id: 2,
title: 'Useful subreddits',
detail: 'Last edit: January 12th, 2015',
},
],
events: [
{
id: 1,
title: 'Group Meeting',
detail: 'In 32 Minutes, Room 1B',
},
{
id: 2,
title: 'Public Beta Release',
detail: '11:00 PM',
},
{
id: 3,
title: 'Dinner with David',
detail: '17:30 PM',
},
{
id: 4,
title: 'Q&A Session',
detail: '20:30 PM',
},
],
},
reducers: {},
});
export const selectQuickPanelData = ({ quickPanel }) => quickPanel.data;
export default dataSlice.reducer;

View File

@@ -0,0 +1,9 @@
import { combineReducers } from '@reduxjs/toolkit';
import data from './dataSlice';
import state from './stateSlice';
const reducer = combineReducers({
data,
state,
});
export default reducer;

View File

@@ -0,0 +1,17 @@
import { createSlice } from '@reduxjs/toolkit';
const stateSlice = createSlice({
name: 'quickPanel/state',
initialState: false,
reducers: {
toggleQuickPanel: (state, action) => !state,
openQuickPanel: (state, action) => true,
closeQuickPanel: (state, action) => false,
},
});
export const { toggleQuickPanel, openQuickPanel, closeQuickPanel } = stateSlice.actions;
export const selectQuickPanelState = ({ quickPanel }) => quickPanel.state;
export default stateSlice.reducer;

View File

@@ -0,0 +1,11 @@
import layout1 from './layout1/Layout1Config';
import layout2 from './layout2/Layout2Config';
import layout3 from './layout3/Layout3Config';
const themeLayoutConfigs = {
layout1,
layout2,
layout3,
};
export default themeLayoutConfigs;

View File

@@ -0,0 +1,11 @@
import layout1 from './layout1/Layout1';
import layout2 from './layout2/Layout2';
import layout3 from './layout3/Layout3';
const themeLayouts = {
layout1,
layout2,
layout3,
};
export default themeLayouts;