🎨 style: add animation when sidebar open on mobile
This commit is contained in:
parent
1648deb64f
commit
b03200c256
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user