init: add fuse-react v8.3.5 skeleton
This commit is contained in:
76
src/app/theme-layouts/layout1/Layout1.js
Normal file
76
src/app/theme-layouts/layout1/Layout1.js
Normal 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);
|
||||
152
src/app/theme-layouts/layout1/Layout1Config.js
Normal file
152
src/app/theme-layouts/layout1/Layout1Config.js
Normal 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;
|
||||
33
src/app/theme-layouts/layout1/components/FooterLayout1.js
Normal file
33
src/app/theme-layouts/layout1/components/FooterLayout1.js
Normal 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);
|
||||
@@ -0,0 +1,7 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
function LeftSideLayout1() {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export default memo(LeftSideLayout1);
|
||||
@@ -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);
|
||||
15
src/app/theme-layouts/layout1/components/RightSideLayout1.js
Normal file
15
src/app/theme-layouts/layout1/components/RightSideLayout1.js
Normal 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);
|
||||
103
src/app/theme-layouts/layout1/components/ToolbarLayout1.js
Normal file
103
src/app/theme-layouts/layout1/components/ToolbarLayout1.js
Normal 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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
84
src/app/theme-layouts/layout2/Layout2.js
Normal file
84
src/app/theme-layouts/layout2/Layout2.js
Normal 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);
|
||||
138
src/app/theme-layouts/layout2/Layout2Config.js
Normal file
138
src/app/theme-layouts/layout2/Layout2Config.js
Normal 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;
|
||||
28
src/app/theme-layouts/layout2/components/FooterLayout2.js
Normal file
28
src/app/theme-layouts/layout2/components/FooterLayout2.js
Normal 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);
|
||||
@@ -0,0 +1,7 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
function LeftSideLayout2() {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export default memo(LeftSideLayout2);
|
||||
29
src/app/theme-layouts/layout2/components/NavbarLayout2.js
Normal file
29
src/app/theme-layouts/layout2/components/NavbarLayout2.js
Normal 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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
15
src/app/theme-layouts/layout2/components/RightSideLayout2.js
Normal file
15
src/app/theme-layouts/layout2/components/RightSideLayout2.js
Normal 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);
|
||||
70
src/app/theme-layouts/layout2/components/ToolbarLayout2.js
Normal file
70
src/app/theme-layouts/layout2/components/ToolbarLayout2.js
Normal 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);
|
||||
83
src/app/theme-layouts/layout3/Layout3.js
Normal file
83
src/app/theme-layouts/layout3/Layout3.js
Normal 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);
|
||||
139
src/app/theme-layouts/layout3/Layout3Config.js
Normal file
139
src/app/theme-layouts/layout3/Layout3Config.js
Normal 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;
|
||||
28
src/app/theme-layouts/layout3/components/FooterLayout3.js
Normal file
28
src/app/theme-layouts/layout3/components/FooterLayout3.js
Normal 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);
|
||||
15
src/app/theme-layouts/layout3/components/LeftSideLayout3.js
Normal file
15
src/app/theme-layouts/layout3/components/LeftSideLayout3.js
Normal 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);
|
||||
24
src/app/theme-layouts/layout3/components/NavbarLayout3.js
Normal file
24
src/app/theme-layouts/layout3/components/NavbarLayout3.js
Normal 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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
15
src/app/theme-layouts/layout3/components/RightSideLayout3.js
Normal file
15
src/app/theme-layouts/layout3/components/RightSideLayout3.js
Normal 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);
|
||||
78
src/app/theme-layouts/layout3/components/ToolbarLayout3.js
Normal file
78
src/app/theme-layouts/layout3/components/ToolbarLayout3.js
Normal 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);
|
||||
89
src/app/theme-layouts/shared-components/AdjustFontSize.js
Normal file
89
src/app/theme-layouts/shared-components/AdjustFontSize.js
Normal 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;
|
||||
@@ -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;
|
||||
96
src/app/theme-layouts/shared-components/FullScreenToggle.js
Normal file
96
src/app/theme-layouts/shared-components/FullScreenToggle.js
Normal 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;
|
||||
@@ -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;
|
||||
88
src/app/theme-layouts/shared-components/LanguageSwitcher.js
Normal file
88
src/app/theme-layouts/shared-components/LanguageSwitcher.js
Normal 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;
|
||||
39
src/app/theme-layouts/shared-components/Logo.js
Normal file
39
src/app/theme-layouts/shared-components/Logo.js
Normal 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;
|
||||
@@ -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;
|
||||
89
src/app/theme-layouts/shared-components/NavbarToggleFab.js
Normal file
89
src/app/theme-layouts/shared-components/NavbarToggleFab.js
Normal 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;
|
||||
39
src/app/theme-layouts/shared-components/Navigation.js
Normal file
39
src/app/theme-layouts/shared-components/Navigation.js
Normal 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);
|
||||
12
src/app/theme-layouts/shared-components/NavigationSearch.js
Normal file
12
src/app/theme-layouts/shared-components/NavigationSearch.js
Normal 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;
|
||||
@@ -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;
|
||||
107
src/app/theme-layouts/shared-components/PoweredByLinks.js
Normal file
107
src/app/theme-layouts/shared-components/PoweredByLinks.js
Normal file
File diff suppressed because one or more lines are too long
23
src/app/theme-layouts/shared-components/PurchaseButton.js
Normal file
23
src/app/theme-layouts/shared-components/PurchaseButton.js
Normal 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;
|
||||
202
src/app/theme-layouts/shared-components/SettingsPanel.js
Normal file
202
src/app/theme-layouts/shared-components/SettingsPanel.js
Normal 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);
|
||||
115
src/app/theme-layouts/shared-components/UserMenu.js
Normal file
115
src/app/theme-layouts/shared-components/UserMenu.js
Normal 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;
|
||||
56
src/app/theme-layouts/shared-components/UserNavbarHeader.js
Normal file
56
src/app/theme-layouts/shared-components/UserNavbarHeader.js
Normal 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;
|
||||
229
src/app/theme-layouts/shared-components/chatPanel/Chat.js
Normal file
229
src/app/theme-layouts/shared-components/chatPanel/Chat.js
Normal 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;
|
||||
227
src/app/theme-layouts/shared-components/chatPanel/ChatPanel.js
Normal file
227
src/app/theme-layouts/shared-components/chatPanel/ChatPanel.js
Normal 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));
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
106
src/app/theme-layouts/shared-components/chatPanel/ContactList.js
Normal file
106
src/app/theme-layouts/shared-components/chatPanel/ContactList.js
Normal 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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
144
src/app/theme-layouts/shared-components/quickPanel/QuickPanel.js
Normal file
144
src/app/theme-layouts/shared-components/quickPanel/QuickPanel.js
Normal 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));
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
11
src/app/theme-layouts/themeLayoutConfigs.js
Normal file
11
src/app/theme-layouts/themeLayoutConfigs.js
Normal 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;
|
||||
11
src/app/theme-layouts/themeLayouts.js
Normal file
11
src/app/theme-layouts/themeLayouts.js
Normal 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;
|
||||
Reference in New Issue
Block a user