🎨 style: add animation when sidebar open on mobile

This commit is contained in:
Wahyu Kurniawan 2024-03-06 09:34:41 +07:00
parent 1648deb64f
commit b03200c256
No known key found for this signature in database
GPG Key ID: 040A1549143A8E33
2 changed files with 107 additions and 25 deletions

View File

@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { NavLink, useNavigate, useParams } from 'react-router-dom'; import { NavLink, useNavigate, useParams } from 'react-router-dom';
import { Organization, User } from 'gql-client'; import { Organization, User } from 'gql-client';
import { motion } from 'framer-motion';
import { Option } from '@material-tailwind/react'; import { Option } from '@material-tailwind/react';
import { useDisconnect } from 'wagmi'; import { useDisconnect } from 'wagmi';
@ -22,12 +23,19 @@ import { Avatar } from 'components/shared/Avatar';
import { formatAddress } from 'utils/format'; import { formatAddress } from 'utils/format';
import { getInitials } from 'utils/geInitials'; import { getInitials } from 'utils/geInitials';
import { Button } from 'components/shared/Button'; import { Button } from 'components/shared/Button';
import { cn } from 'utils/classnames';
import { useMediaQuery } from 'usehooks-ts';
export const Sidebar = () => { interface SidebarProps {
mobileOpen?: boolean;
}
export const Sidebar = ({ mobileOpen }: SidebarProps) => {
const { orgSlug } = useParams(); const { orgSlug } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const client = useGQLClient(); const client = useGQLClient();
const { disconnect } = useDisconnect(); const { disconnect } = useDisconnect();
const isDesktop = useMediaQuery('(min-width: 1024px)');
const [user, setUser] = useState<User>(); const [user, setUser] = useState<User>();
@ -57,11 +65,24 @@ export const Sidebar = () => {
disconnect(); disconnect();
navigate('/login'); navigate('/login');
}, [disconnect, navigate]); }, [disconnect, navigate]);
console.log(isDesktop);
return ( return (
<nav className="h-full w-[320px] lg:flex hidden flex-col"> <motion.nav
<div className="flex flex-col h-full pt-8 pb-0 px-6 lg:pb-8 gap-9"> initial={{ x: -320 }}
<Logo orgSlug={orgSlug} /> animate={{ x: isDesktop || mobileOpen ? 0 : -320 }}
exit={{ x: -320 }}
transition={{ type: 'spring', stiffness: 260, damping: 20 }}
className={cn('h-full flex-none w-[320px] lg:flex hidden flex-col', {
flex: mobileOpen,
})}
>
<div
className={cn('flex flex-col h-full pt-8 pb-0 px-6 lg:pb-8 gap-9', {
'px-4 pt-5': mobileOpen,
})}
>
{/* Logo */}
{!mobileOpen && <Logo orgSlug={orgSlug} />}
{/* Switch organization */} {/* Switch organization */}
<div className="flex flex-1 flex-col gap-4"> <div className="flex flex-1 flex-col gap-4">
<AsyncSelect <AsyncSelect
@ -156,6 +177,7 @@ export const Sidebar = () => {
</Tabs> </Tabs>
</div> </div>
</div> </div>
{/* Only shows when on mobile */}
<div className="shadow-card-sm py-4 pl-4 pr-2 flex lg:hidden items-center border-t border-border-separator/[0.06]"> <div className="shadow-card-sm py-4 pl-4 pr-2 flex lg:hidden items-center border-t border-border-separator/[0.06]">
{user?.name && ( {user?.name && (
<div className="flex items-center flex-1 gap-3"> <div className="flex items-center flex-1 gap-3">
@ -169,6 +191,7 @@ export const Sidebar = () => {
</div> </div>
)} )}
<Button <Button
iconOnly
variant="ghost" variant="ghost"
className="text-elements-low-em" className="text-elements-low-em"
onClick={handleLogOut} onClick={handleLogOut}
@ -176,6 +199,6 @@ export const Sidebar = () => {
<LogoutIcon /> <LogoutIcon />
</Button> </Button>
</div> </div>
</nav> </motion.nav>
); );
}; };

View File

@ -1,25 +1,35 @@
import { Logo } from 'components/Logo'; import { Logo } from 'components/Logo';
import { Button } from 'components/shared/Button'; import { Button } from 'components/shared/Button';
import { import {
CrossIcon,
MenuIcon, MenuIcon,
NotificationBellIcon, NotificationBellIcon,
SearchIcon, SearchIcon,
} from 'components/shared/CustomIcon'; } from 'components/shared/CustomIcon';
import { Sidebar } from 'components/shared/Sidebar'; import { Sidebar } from 'components/shared/Sidebar';
import { OctokitProvider } from 'context/OctokitContext'; import { OctokitProvider } from 'context/OctokitContext';
import React, { ComponentPropsWithoutRef } from 'react'; import React, { ComponentPropsWithoutRef, useEffect, useState } from 'react';
import { Outlet, useParams } from 'react-router-dom'; import { Outlet, useParams } from 'react-router-dom';
import { AnimatePresence, motion } from 'framer-motion';
import { cn } from 'utils/classnames'; import { cn } from 'utils/classnames';
import { useMediaQuery } from 'usehooks-ts';
export interface DashboardLayoutProps export interface DashboardLayoutProps
extends ComponentPropsWithoutRef<'section'> {} extends ComponentPropsWithoutRef<'section'> {}
export const DashboardLayout = ({ export const DashboardLayout = ({
className, className,
children,
...props ...props
}: DashboardLayoutProps) => { }: DashboardLayoutProps) => {
const { orgSlug } = useParams(); const { orgSlug } = useParams();
const isDesktop = useMediaQuery('(min-width: 1024px)');
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
useEffect(() => {
if (isDesktop) {
setIsSidebarOpen(false);
}
}, [isDesktop]);
return ( return (
<section <section
@ -29,30 +39,79 @@ export const DashboardLayout = ({
className, className,
)} )}
> >
<Sidebar />
{/* Header on mobile */} {/* Header on mobile */}
<div className="flex lg:hidden items-center px-4 py-4 justify-between"> <div className="flex lg:hidden items-center px-4 py-4 justify-between">
<Logo orgSlug={orgSlug} /> <Logo orgSlug={orgSlug} />
<div className="flex items-center gap-0.5"> <div className="flex items-center gap-0.5">
<Button iconOnly variant="ghost"> <AnimatePresence>
<NotificationBellIcon size={18} /> {isSidebarOpen ? (
</Button> <motion.div
<Button iconOnly variant="ghost"> key="crossIcon"
<SearchIcon size={18} /> initial={{ opacity: 0 }}
</Button> animate={{
<Button iconOnly variant="ghost"> opacity: 1,
<MenuIcon size={18} /> transition: { duration: 0.2, delay: 0.3 },
</Button> }}
exit={{ opacity: 0, transition: { duration: 0 } }}
>
<Button
iconOnly
variant="ghost"
onClick={() => setIsSidebarOpen(false)}
>
<CrossIcon size={20} />
</Button>
</motion.div>
) : (
<motion.div
key="menuIcons"
initial={{ opacity: 0 }}
animate={{
opacity: 1,
transition: { duration: 0.2, delay: 0.2 },
}}
exit={{ opacity: 0, transition: { duration: 0 } }}
>
<>
<Button iconOnly variant="ghost">
<NotificationBellIcon size={18} />
</Button>
<Button iconOnly variant="ghost">
<SearchIcon size={18} />
</Button>
<Button
iconOnly
variant="ghost"
onClick={() => setIsSidebarOpen(true)}
>
<MenuIcon size={18} />
</Button>
</>
</motion.div>
)}
</AnimatePresence>
</div> </div>
</div> </div>
<div className="flex-1 w-full h-full px-1 py-1 md:px-3 md:py-3 overflow-y-hidden"> <div className="flex h-full w-full overflow-hidden">
<div className="rounded-3xl bg-base-bg h-full shadow-card overflow-y-auto relative"> <Sidebar mobileOpen={isSidebarOpen} />
<OctokitProvider> <motion.div
<Outlet /> className={cn(
</OctokitProvider> 'w-full h-full pr-1 pl-1 py-1 md:pl-0 md:pr-3 md:py-3 overflow-y-hidden min-w-[320px]',
</div> { 'flex-shrink-0': isSidebarOpen || !isDesktop },
)}
initial={{ x: 0 }} // Initial state, no translation
animate={{
x: isSidebarOpen ? '10px' : 0, // Translate X based on sidebar state
}}
transition={{ type: 'spring', stiffness: 260, damping: 20 }}
>
<div className="rounded-3xl bg-base-bg h-full shadow-card overflow-y-auto relative">
<OctokitProvider>
<Outlet />
</OctokitProvider>
</div>
</motion.div>
</div> </div>
{children}
</section> </section>
); );
}; };