diff --git a/.changeset/flat-books-peel.md b/.changeset/flat-books-peel.md new file mode 100644 index 0000000..9ce3a54 --- /dev/null +++ b/.changeset/flat-books-peel.md @@ -0,0 +1,5 @@ +--- +"mantine-analytics-dashboard": patch +--- + +feat: multiple sidebar variations diff --git a/.eslintrc.json b/.eslintrc.json index 36ffe35..d66bee2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -46,16 +46,6 @@ "single" ] } - ], - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/ban-ts-comment": "warn", - "@typescript-eslint/no-unused-vars": [ - "error", - { - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_", - "caughtErrorsIgnorePattern": "^_" - } ] } } diff --git a/app/apps/layout.tsx b/app/apps/layout.tsx index dd888ba..b0a977a 100644 --- a/app/apps/layout.tsx +++ b/app/apps/layout.tsx @@ -1,64 +1,15 @@ -'use client'; - -import { AppShell, Container, rem, useMantineTheme } from '@mantine/core'; import { ReactNode } from 'react'; -import { useDisclosure, useMediaQuery } from '@mantine/hooks'; -import AppMain from '@/components/AppMain'; -import Navigation from '@/components/Navigation'; -import HeaderNav from '@/components/HeaderNav'; -import FooterNav from '@/components/FooterNav'; + +import { MainLayout } from '@/layout/Main'; + +export type SidebarState = 'hidden' | 'mini' | 'full'; type Props = { children: ReactNode; }; function AppsLayout({ children }: Props) { - const theme = useMantineTheme(); - const tablet_match = useMediaQuery('(max-width: 768px)'); - const [mobileOpened, { toggle: toggleMobile }] = useDisclosure(); - const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true); - - return ( - - - - - - - - - - - {children} - - - - - - - - ); + return {children}; } export default AppsLayout; diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx index 1a120ef..99261c7 100644 --- a/app/dashboard/layout.tsx +++ b/app/dashboard/layout.tsx @@ -1,64 +1,13 @@ -'use client'; +import { ReactNode } from 'react'; -import { AppShell, Container, rem, useMantineTheme } from '@mantine/core'; -import { ReactNode, useState } from 'react'; -import { useDisclosure, useMediaQuery } from '@mantine/hooks'; -import AppMain from '@/components/AppMain'; -import Navigation from '@/components/Navigation'; -import HeaderNav from '@/components/HeaderNav'; -import FooterNav from '@/components/FooterNav'; +import { MainLayout } from '@/layout/Main'; type Props = { children: ReactNode; }; function DashboardLayout({ children }: Props) { - const theme = useMantineTheme(); - const tablet_match = useMediaQuery('(max-width: 768px)'); - const [mobileOpened, { toggle: toggleMobile }] = useDisclosure(); - const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true); - - return ( - - - - - - - - - - - {children} - - - - - - - - ); + return {children}; } export default DashboardLayout; diff --git a/app/layout.tsx b/app/layout.tsx index c2c9c89..9e82cc2 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,6 +4,7 @@ import { ColorSchemeScript, MantineProvider } from '@mantine/core'; import { ModalsProvider } from '@mantine/modals'; import { Notifications } from '@mantine/notifications'; import { Open_Sans } from 'next/font/google'; + import { myTheme } from '@/theme'; import '@mantine/core/styles.css'; import '@mantine/dates/styles.css'; diff --git a/app/pages/layout.tsx b/app/pages/layout.tsx index 12e80ad..045a884 100644 --- a/app/pages/layout.tsx +++ b/app/pages/layout.tsx @@ -1,64 +1,13 @@ -'use client'; +import { ReactNode } from 'react'; -import { AppShell, Container, rem, useMantineTheme } from '@mantine/core'; -import { ReactNode, useState } from 'react'; -import { useDisclosure, useMediaQuery } from '@mantine/hooks'; -import AppMain from '@/components/AppMain'; -import Navigation from '@/components/Navigation'; -import HeaderNav from '@/components/HeaderNav'; -import FooterNav from '@/components/FooterNav'; +import { MainLayout } from '@/layout/Main'; type Props = { children: ReactNode; }; function PagesLayout({ children }: Props) { - const theme = useMantineTheme(); - const tablet_match = useMediaQuery('(max-width: 768px)'); - const [mobileOpened, { toggle: toggleMobile }] = useDisclosure(); - const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true); - - return ( - - - - - - - - - - - {children} - - - - - - - - ); + return {children}; } export default PagesLayout; diff --git a/components/HeaderNav/HeaderNav.tsx b/components/HeaderNav/HeaderNav.tsx index d299b88..a72a521 100644 --- a/components/HeaderNav/HeaderNav.tsx +++ b/components/HeaderNav/HeaderNav.tsx @@ -7,30 +7,28 @@ import { Flex, Group, Indicator, - MantineTheme, Menu, - rem, Stack, Text, TextInput, Tooltip, + rem, useMantineColorScheme, useMantineTheme, } from '@mantine/core'; +import { useMediaQuery } from '@mantine/hooks'; import { IconBell, IconCircleHalf2, - IconLayoutSidebarLeftCollapse, - IconLayoutSidebarLeftExpand, IconMessageCircle, IconMoonStars, IconPower, IconSearch, IconSunHigh, } from '@tabler/icons-react'; + +import { SidebarState } from '@/app/apps/layout'; import { LanguagePicker } from '@/components'; -import { upperFirst, useMediaQuery } from '@mantine/hooks'; -import { showNotification } from '@mantine/notifications'; const ICON_SIZE = 20; @@ -131,15 +129,14 @@ const NOTIFICATIONS = [ type HeaderNavProps = { mobileOpened?: boolean; toggleMobile?: () => void; - desktopOpened?: boolean; - toggleDesktop?: () => void; + sidebarState: SidebarState; + onSidebarStateChange: () => void; }; const HeaderNav = (props: HeaderNavProps) => { - const { desktopOpened, toggleDesktop, toggleMobile, mobileOpened } = props; + const { toggleMobile, mobileOpened, onSidebarStateChange } = props; const theme = useMantineTheme(); const { setColorScheme, colorScheme } = useMantineColorScheme(); - const laptop_match = useMediaQuery('(max-width: 992px)'); const tablet_match = useMediaQuery('(max-width: 768px)'); const mobile_match = useMediaQuery('(max-width: 425px)'); @@ -198,69 +195,11 @@ const HeaderNav = (props: HeaderNavProps) => { )); - const handleColorSwitch = (mode: 'light' | 'dark' | 'auto') => { - setColorScheme(mode); - showNotification({ - title: `${upperFirst(mode)} is on`, - message: `You just switched to ${ - colorScheme === 'dark' ? 'light' : 'dark' - } mode. Hope you like it`, - styles: (theme: MantineTheme) => ({ - root: { - backgroundColor: - colorScheme === 'dark' - ? theme.colors.gray[7] - : theme.colors.gray[2], - borderColor: - colorScheme === 'dark' - ? theme.colors.gray[7] - : theme.colors.gray[2], - - '&::before': { - backgroundColor: - colorScheme === 'dark' - ? theme.colors.gray[2] - : theme.colors.gray[7], - }, - }, - - title: { - color: - colorScheme === 'dark' - ? theme.colors.gray[2] - : theme.colors.gray[7], - }, - description: { - color: - colorScheme === 'dark' - ? theme.colors.gray[2] - : theme.colors.gray[7], - }, - closeButton: { - color: - colorScheme === 'dark' - ? theme.colors.gray[2] - : theme.colors.gray[7], - '&:hover': { - backgroundColor: theme.colors.red[5], - color: theme.white, - }, - }, - }), - }); - }; - return ( - - {desktopOpened ? ( - - ) : ( - - )} - + { +const Logo = ({ href, showText = true, ...others }: LogoProps) => { return ( { design sparx logo - Mantine admin + {showText && Mantine admin} ); diff --git a/components/Navigation/Links/Links.module.css b/components/Navigation/Links/Links.module.css index 5d65072..57d19a5 100644 --- a/components/Navigation/Links/Links.module.css +++ b/components/Navigation/Links/Links.module.css @@ -10,7 +10,7 @@ } @mixin dark { - color: var(--mantine-color-black); + color: var(--mantine-color-white); } @mixin hover { @@ -40,6 +40,14 @@ color: var(--mantine-primary-color-light-color); } } + + &[data-mini="true"] { + width: rem(67px); + } + } + + &[data-mini="true"] { + text-align: center; } } @@ -57,7 +65,7 @@ } @mixin dark { - color: var(--mantine-color-black); + color: var(--mantine-color-white); } @mixin hover { diff --git a/components/Navigation/Links/Links.tsx b/components/Navigation/Links/Links.tsx index d4e2600..7bcb5c8 100644 --- a/components/Navigation/Links/Links.tsx +++ b/components/Navigation/Links/Links.tsx @@ -1,8 +1,18 @@ -import { useEffect, useState } from 'react'; -import { Box, Collapse, Group, Text, UnstyledButton } from '@mantine/core'; +import { useEffect, useMemo, useState } from 'react'; + +import { + Box, + Collapse, + Group, + Menu, + Text, + Tooltip, + UnstyledButton, +} from '@mantine/core'; import { IconChevronRight } from '@tabler/icons-react'; -import { usePathname, useRouter } from 'next/navigation'; import * as _ from 'lodash'; +import { usePathname, useRouter } from 'next/navigation'; + import classes from './Links.module.css'; interface LinksGroupProps { @@ -15,6 +25,7 @@ interface LinksGroupProps { link: string; }[]; closeSidebar: () => void; + isMini?: boolean; } export function LinksGroup(props: LinksGroupProps) { @@ -25,6 +36,7 @@ export function LinksGroup(props: LinksGroupProps) { link, links, closeSidebar, + isMini, } = props; const router = useRouter(); const pathname = usePathname(); @@ -33,20 +45,118 @@ export function LinksGroup(props: LinksGroupProps) { const [currentPath, setCurrentPath] = useState(); const ChevronIcon = IconChevronRight; - const items = (hasLinks ? links : []).map((link) => ( - { - router.push(link.link); - closeSidebar(); - }} - key={link.label} - data-active={link.link.toLowerCase() === pathname || undefined} - > - {link.label} - - )); + const LinkItem = ({ link }: { link: { label: string; link: string } }) => { + return ( + { + router.push(link.link); + closeSidebar(); + }} + data-active={link.link.toLowerCase() === pathname || undefined} + data-mini={isMini} + > + {link.label} + + ); + }; + + const items = (hasLinks ? links : []).map((link) => + isMini ? ( + + + + ) : ( + + ), + ); + + const content: React.ReactElement = useMemo(() => { + let view: React.ReactElement; + if (isMini) { + view = ( + <> + + + { + setOpened((o) => !o); + link && router.push(link || '#'); + closeSidebar(); + }} + className={classes.control} + data-active={opened || undefined} + data-mini={isMini} + > + + + + + + {items} + + + ); + } else { + view = ( + <> + { + setOpened((o) => !o); + link && router.push(link || '#'); + closeSidebar(); + }} + className={classes.control} + data-active={opened || undefined} + data-mini={isMini} + > + + + + {!isMini && {label}} + + {hasLinks && ( + + )} + + + {hasLinks ? {items} : null} + + ); + } + + return view; + }, [ + ChevronIcon, + Icon, + closeSidebar, + hasLinks, + isMini, + items, + label, + link, + opened, + router, + ]); useEffect(() => { const paths = pathname.split('/'); @@ -54,35 +164,5 @@ export function LinksGroup(props: LinksGroupProps) { setCurrentPath(_.last(paths)?.toLowerCase() || undefined); }, [pathname, label]); - return ( - <> - { - setOpened((o) => !o); - link && router.push(link || '#'); - closeSidebar(); - }} - className={classes.control} - data-active={opened || undefined} - > - - - - {label} - - {hasLinks && ( - - )} - - - {hasLinks ? {items} : null} - - ); + return <>{content}; } diff --git a/components/Navigation/Navigation.module.css b/components/Navigation/Navigation.module.css index 52718cf..023c539 100644 --- a/components/Navigation/Navigation.module.css +++ b/components/Navigation/Navigation.module.css @@ -1,88 +1,106 @@ .navbar { + background-color: var(--mantine-primary-color-filled); + min-height: rem(100vh); + padding: var(--mantine-spacing-md); + padding-bottom: 0; + display: flex; + flex-direction: column; + border-right: rem(1px) solid light-dark( + var(--mantine-primary-color-filled-hover), + var(--mantine-color-dark-4) + ); + + @mixin light { background-color: var(--mantine-primary-color-filled); - min-height: rem(100vh); + } + + @mixin dark { + background-color: var(--mantine-color-dark-7); + } + + &[data-sidebar-state="full"] { width: rem(300px); - padding: var(--mantine-spacing-md); - padding-bottom: 0; - display: flex; - flex-direction: column; - border-right: rem(1px) solid light-dark( - var(--mantine-primary-color-filled-hover), - var(--mantine-color-dark-4) - ); - - @mixin light { - background-color: var(--mantine-primary-color-filled); - } - @mixin dark { - background-color: var(--mantine-primary-color-light-color); + @media (max-width: $mantine-breakpoint-md) { + width: 100%; } - @media (max-width: $mantine-breakpoint-md) { - width: 100%; + @media (max-width: $mantine-breakpoint-sm) { + width: 100%; } -} + } -.header { - padding: var(--mantine-spacing-md); - padding-top: 0; - margin-left: calc(var(--mantine-spacing-md) * -1); - margin-right: calc(var(--mantine-spacing-md) * -1); - border-bottom: rem(1px) solid var(--mantine-primary-color-filled-hover); + &[data-sidebar-state="mini"] { + width: rem(60px); + padding: var(--mantine-spacing-xs); + + .linkLabel, .chevron { + display: none; + } - @mixin light { - color: var(--mantine-color-white); + .link { + padding: var(--mantine-spacing-xs); + border-radius: var(--mantine-radius-sm); + margin-bottom: var(--mantine-spacing-xs); } - @mixin dark { - color: var(--mantine-color-black); + .icon { + margin-right: 0; } + } + + &[data-sidebar-state="hidden"] { + width: 0; + padding: 0; + overflow: hidden; + } + + @media (max-width: $mantine-breakpoint-md) { + width: 100%; + } +} + +.header { + padding: 0 var(--mantine-spacing-md) var(--mantine-spacing-sm) var(--mantine-spacing-md); + margin-left: calc(var(--mantine-spacing-md) * -1); + margin-right: calc(var(--mantine-spacing-md) * -1); + border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); + color: var(--mantine-color-white); } .logo { - @mixin light { - color: var(--mantine-color-white); - } + /*@mixin light {*/ + /* color: var(--mantine-color-white);*/ + /*}*/ - @mixin dark { - color: var(--mantine-color-black); - } + /*@mixin dark {*/ + /* color: var(--mantine-color-black);*/ + /*}*/ } .links { - flex: 1; - margin-left: calc(var(--mantine-spacing-md) * -1); - margin-right: calc(var(--mantine-spacing-md) * -1); + flex: 1; + margin-left: calc(var(--mantine-spacing-md) * -1); + margin-right: calc(var(--mantine-spacing-md) * -1); } .linksInner { - padding-top: var(--mantine-spacing-xl); - padding-bottom: var(--mantine-spacing-xl); + padding-top: var(--mantine-spacing-xl); + padding-bottom: var(--mantine-spacing-xl); + + &[data-sidebar-state="mini"] { + padding-top: 0; + } } .linkHeader { - font-weight: 500; - - @mixin light { - color: var(--mantine-color-gray-3); - } - - @mixin dark { - color: var(--mantine-color-dark-8); - } + font-weight: 500; + color: light-dark(var(--mantine-color-white), var(--mantine-color-gray-3)); } .footer { - margin-left: calc(var(--mantine-spacing-md) * -1); - margin-right: calc(var(--mantine-spacing-md) * -1); - border-top: rem(1px) solid var(--mantine-primary-color-filled-hover); - - @mixin light { - color: var(--mantine-color-white); - } - - @mixin dark { - color: var(--mantine-color-black); - } + margin-left: calc(var(--mantine-spacing-md) * -1); + margin-right: calc(var(--mantine-spacing-md) * -1); + border-top: rem(1px) solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); + color: var(--mantine-color-white); } diff --git a/components/Navigation/Navigation.tsx b/components/Navigation/Navigation.tsx index d083a57..454a42f 100644 --- a/components/Navigation/Navigation.tsx +++ b/components/Navigation/Navigation.tsx @@ -1,4 +1,7 @@ +import { useEffect, useState } from 'react'; + import { ActionIcon, Box, Flex, Group, ScrollArea, Text } from '@mantine/core'; +import { useMediaQuery } from '@mantine/hooks'; import { IconBook2, IconBrandAuth0, @@ -23,7 +26,11 @@ import { IconUserShield, IconX, } from '@tabler/icons-react'; + +import { SidebarState } from '@/app/apps/layout'; import { Logo, UserProfileButton } from '@/components'; +import { LinksGroup } from '@/components/Navigation/Links/Links'; +import UserProfileData from '@/public/mocks/UserProfile.json'; import { PATH_ABOUT, PATH_APPS, @@ -32,10 +39,8 @@ import { PATH_DOCS, PATH_PAGES, } from '@/routes'; -import UserProfileData from '@/public/mocks/UserProfile.json'; -import { useMediaQuery } from '@mantine/hooks'; + import classes from './Navigation.module.css'; -import { LinksGroup } from '@/components/Navigation/Links/Links'; const mockdata = [ { @@ -122,27 +127,36 @@ const mockdata = [ type NavigationProps = { onClose: () => void; + sidebarState: SidebarState; + onSidebarStateChange: (state: SidebarState) => void; }; -const Navigation = ({ onClose }: NavigationProps) => { +const Navigation = ({ + onClose, + onSidebarStateChange, + sidebarState, +}: NavigationProps) => { const tablet_match = useMediaQuery('(max-width: 768px)'); const links = mockdata.map((m) => ( - - - {m.title} - + + {sidebarState !== 'mini' && ( + + {m.title} + + )} {m.links.map((item) => ( { setTimeout(() => { onClose(); @@ -153,15 +167,21 @@ const Navigation = ({ onClose }: NavigationProps) => { )); + useEffect(() => { + if (tablet_match) { + onSidebarStateChange('full'); + } + }, [onSidebarStateChange, tablet_match]); + return ( - + ); }; diff --git a/components/UserButton/UserButton.tsx b/components/UserButton/UserButton.tsx index e075dc5..d27de10 100644 --- a/components/UserButton/UserButton.tsx +++ b/components/UserButton/UserButton.tsx @@ -1,4 +1,5 @@ import { ReactNode } from 'react'; + import { Avatar, Group, @@ -7,6 +8,7 @@ import { UnstyledButtonProps, } from '@mantine/core'; import { IconChevronRight } from '@tabler/icons-react'; + import classes from './UserButton.module.css'; type UserProfileButtonProps = { @@ -15,6 +17,7 @@ type UserProfileButtonProps = { email: string; icon?: ReactNode; asAction?: boolean; + showText?: boolean; } & UnstyledButtonProps; const UserProfileButton = ({ @@ -23,6 +26,7 @@ const UserProfileButton = ({ email, icon, asAction, + showText = true, ...others }: UserProfileButtonProps) => { return ( @@ -30,13 +34,15 @@ const UserProfileButton = ({ -
- - {name} - + {showText && ( +
+ + {name} + - {email} -
+ {email} +
+ )} {icon && asAction && }
diff --git a/layout/Main/index.tsx b/layout/Main/index.tsx new file mode 100644 index 0000000..38c63c8 --- /dev/null +++ b/layout/Main/index.tsx @@ -0,0 +1,81 @@ +'use client'; + +import { ReactNode, useState } from 'react'; + +import { AppShell, Container, rem, useMantineTheme } from '@mantine/core'; +import { useDisclosure, useLocalStorage, useMediaQuery } from '@mantine/hooks'; + +import AppMain from '@/components/AppMain'; +import FooterNav from '@/components/FooterNav'; +import HeaderNav from '@/components/HeaderNav'; +import Navigation from '@/components/Navigation'; + +export type SidebarState = 'hidden' | 'mini' | 'full'; + +type Props = { + children: ReactNode; +}; + +export function MainLayout({ children }: Props) { + const theme = useMantineTheme(); + const tablet_match = useMediaQuery('(max-width: 768px)'); + const [mobileOpened, { toggle: toggleMobile }] = useDisclosure(); + const [desktopOpened] = useDisclosure(true); + const [sidebarState, setSidebarState] = useLocalStorage({ + key: 'mantine-nav-state', + defaultValue: 'full', + }); + + const toggleSidebarState = () => { + setSidebarState((current) => { + if (current === 'full') return 'mini'; + if (current === 'mini') return 'hidden'; + return 'full'; + }); + }; + + return ( + + + + + + + + + + + {children} + + + + + + + + ); +}