add: side menu transition

This commit is contained in:
Mahyar Khanbolooki
2025-07-04 16:50:55 +03:30
parent 424373bc3f
commit 43cf76bec2
9 changed files with 255 additions and 56 deletions
+7 -13
View File
@@ -1,6 +1,6 @@
import BottomNavBar from "@/components/navigation/BottomNavBar";
import { ReactQueryProvider } from "@/components/providers/ReactQueryProvider"; import { ReactQueryProvider } from "@/components/providers/ReactQueryProvider";
import { Menu } from '@/components/menu/Menu'; import ClientSideWrapper from "@/components/wrapper/ClientSideWrapper";
import ClientMenuRouteWrapper from "@/components/wrapper/ClientMenuRouteWrapper.tsx";
export const metadata = { export const metadata = {
title: 'Menu', title: 'Menu',
@@ -14,17 +14,11 @@ export default function MenuLayout({
return ( return (
<ReactQueryProvider> <ReactQueryProvider>
<main className="h-full pb-0"> <ClientSideWrapper>
{children} <ClientMenuRouteWrapper>
</main> {children}
<aside> </ClientMenuRouteWrapper>
</ClientSideWrapper>
<Menu />
</aside>
<footer>
<BottomNavBar />
</footer>
</ReactQueryProvider> </ReactQueryProvider>
); );
} }
+22
View File
@@ -0,0 +1,22 @@
import React from 'react'
type Props = {
} & React.InputHTMLAttributes<HTMLInputElement>;
export default function NightModeSwitch({ checked, onClick }: Props) {
return (
<input
onClick={onClick}
className={`visual-switch relative inline-flex justify-between px-1.5 h-6 w-11 items-center rounded-full transition-colors duration-300 ${checked ? "bg-green-500" : "bg-gray-300"
}`}
>
A
<span
className={`absolute inline-block h-5 w-5 transform rounded-full bg-white transition-transform duration-300 ${checked ? "translate-x-1" : "-translate-x-3.5"
}`}
/>
B
</input>
)
}
-19
View File
@@ -1,19 +0,0 @@
import React from 'react'
import { PiSun, PiMoon } from 'react-icons/pi'
export default function NightModeSwitch({ state, toggle }) {
return (
<button
onClick={toggle}
className={`visual-switch relative inline-flex justify-between px-1.5 h-6 w-11 items-center rounded-full transition-colors duration-300 ${state ? "bg-green-500" : "bg-gray-300"
}`}
>
<PiMoon color={state ? "#FFA800" : "#292D32"} className='z-10' size={12} />
<span
className={`absolute inline-block h-5 w-5 transform rounded-full bg-white transition-transform duration-300 ${state ? "translate-x-1" : "-translate-x-3.5"
}`}
/>
<PiSun color={!state ? "#FFA800" : "#292D32"} className='z-10' size={12} />
</button>
)
}
+35
View File
@@ -0,0 +1,35 @@
import React from 'react';
type Props = React.SVGProps<SVGSVGElement>;
const BurgerMenuIcon = (props: Props) => (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M3 7H21"
stroke="#292D32"
strokeWidth="1.5"
strokeLinecap="round"
/>
<path
d="M3 12H21"
stroke="#292D32"
strokeWidth="1.5"
strokeLinecap="round"
/>
<path
d="M3 17H21"
stroke="#292D32"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
);
export default BurgerMenuIcon;
+14 -5
View File
@@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { useState } from 'react' import React, { useMemo } from 'react'
import MenuItemType from './MenuItem' import MenuItemType from './MenuItem'
import MenuItem from './MenuItem'; import MenuItem from './MenuItem';
import NotificationBellIcon from '../icons/NotificationBellIcon'; import NotificationBellIcon from '../icons/NotificationBellIcon';
@@ -63,12 +63,21 @@ const menuItems: Array<Array<MenuItemType>> = [
] ]
] ]
export function Menu() { type Props = {
const [menuState, setMenuState] = useState(true); menuState: boolean,
toggleMenuState: React.MouseEventHandler<HTMLDivElement> | undefined
}
export function Menu({ menuState, toggleMenuState }: Props) {
const menuStateMemo = useMemo(() => menuState, [menuState]);
return ( return (
<BlurredOverlayContainer bgOpacity={40} onClick={() => setMenuState((state) => !state)} visible={menuState}> <BlurredOverlayContainer bgOpacity='40' visible={menuStateMemo} changeDelay='300'>
<div data-visible={menuState} onClick={() => setMenuState((state) => !state)} className='absolute top-0 -right-full data-[visible]:right-0 transition-all duration-200 ease-in-out not-xl:!rounded-s-none overflow-clip bg-white w-[200px] h-full flex flex-col z-40'> <div className='w-full h-full' onClick={toggleMenuState}></div>
<div
data-isvisible={menuStateMemo}
onClick={(e) => { e.stopPropagation(); }}
className={`absolute top-0 -right-full data-[isvisible=true]:right-0 transition-all duration-300 ease-in-out not-xl:!rounded-s-none overflow-clip bg-white w-[200px] h-full flex flex-col z-40`}>
<ul className="overflow-y-auto hide-scrollbar pt-20 pb-10 flex flex-col justify-between h-full "> <ul className="overflow-y-auto hide-scrollbar pt-20 pb-10 flex flex-col justify-between h-full ">
<li> <li>
<div className='flex flex-col gap-[60px]'> <div className='flex flex-col gap-[60px]'>
@@ -1,36 +1,52 @@
'use client' 'use client'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import PingAnimation from './animations/PingAnimation';
type Props = { type Props = {
visible?: boolean; visible?: boolean
bgOpacity?: number; bgOpacity?: string
delay?: number; changeDelay?: string
} & React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> effectdelay?: string
} & React.DetailedHTMLProps<React.HTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
const BlurredOverlayContainer = ({ visible = true, bgOpacity = 30, delay = 0, children = <PingAnimation /> }: Props) => { const BlurredOverlayContainer = ({
const [loaded, setLoaded] = useState(false); visible = true,
bgOpacity = '30',
changeDelay = '0',
effectdelay = '0',
children,
...props
}: Props) => {
const [loaded, setLoaded] = useState(visible)
useEffect(() => { useEffect(() => {
// Trigger fade-in after mount if (visible) {
const timeout = setTimeout(() => setLoaded(visible), 300); // Show immediately or after delay
return () => clearTimeout(timeout); const timeout = setTimeout(() => setLoaded(true), +changeDelay)
}, [visible]); return () => clearTimeout(timeout)
} else {
// Hide with delay (matches opacity transition duration)
const timeout = setTimeout(() => setLoaded(false), +changeDelay) // 300ms matches Tailwind transition
return () => clearTimeout(timeout)
}
}, [visible, changeDelay])
const zClass =
visible || loaded ? 'z-50' : !visible && !loaded ? '-z-50' : 'z-0'
return ( return (
<div <button
data-visible={visible} data-visible={visible}
data-loaded={loaded} data-loaded={loaded}
className={`absolute inset-0 flex items-center justify-center className={`absolute inset-0 flex items-center justify-center
backdrop-blur-sm bg-black/${bgOpacity} backdrop-blur-sm bg-black/${bgOpacity}
opacity-0 transition-opacity duration-300 ease-in-out delay-${delay} opacity-0 transition-opacity duration-300 ease-in-out delay-${effectdelay}
data-[loaded=false]:-z-50 ${zClass}
data-[visible=true]:z-50 ${visible ? 'opacity-100' : 'opacity-0'}`}
data-[visible=true]:opacity-100`} {...props}
> >
{children} {children}
</div> </button>
) )
} }
+65
View File
@@ -0,0 +1,65 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
'use client'
import React from 'react'
import NightModeSwitch from '../button/NightModeSwitch'
import BurgerMenuIcon from '../icons/BurgerMenuIcon'
type Props = {
profileDropState: boolean,
toggleProfileDropState: React.MouseEventHandler<HTMLButtonElement> | undefined
menuState: boolean,
toggleMenuState: React.MouseEventHandler<HTMLButtonElement> | undefined
searchModalState: boolean,
toggleSearchModalState: React.MouseEventHandler<HTMLDivElement> | undefined
nightModeState: boolean,
toggleNightModeState: React.MouseEventHandler<HTMLDivElement> | undefined
}
function TopBar({ profileDropState, toggleProfileDropState, menuState, toggleMenuState, searchModalState, toggleSearchModalState, nightModeState, toggleNightModeState }: Props) {
return (
<div className="p-4">
<div className='bg-white rounded-[15px] w-full h-12 lg:h-24 px-4 sm:p-6 xl:gap-8 items-center flex justify-between overflow-auto'>
<div className='inline-flex justify-between items-center'>
<button className='inline-block xl:hidden' onClick={toggleMenuState}>
<BurgerMenuIcon />
</button>
<div onClick={toggleSearchModalState} className='xl:bg-[#EEF0F7] xl:hidden inline-flex rounded-xl px-4 h-12 xl:w-[370px] items-center content-center' >
S
</div>
<div
className={`z-50 top-0 left-0 w-full h-full px-5 py-15 md:px-15 xl:p-0 fixed xl:relative bg-[#00000044] xl:bg-transparent backdrop-blur-sm xl:backdrop-blur-none justify-center xl:block ${searchModalState ? 'flex' : 'hidden'}`}>
<div
className={`w-full h-full absolute`}
onClick={toggleSearchModalState}>
</div>
<div className='z-55 bg-white w-full md:w-min xl:bg-transparent p-5 xl:p-0 items-center flex xl:block rounded-lg h-max'>
X
</div>
</div>
</div>
<div className='flex items-center justify-between gap-6'>
{/* <NightModeSwitch checked={nightMode} onClick={toggleNightMode} /> */}
{/* <img className='hidden sm:block' src={rectangles} /> */}
<div className='bg-[#EEF0F7] items-center inline-flex min-w-fit p-0.5 rounded-xl'>
<div className='text-[12px] px-2.5 text-nowrap hidden xl:block'>
۲,۰۰۰,۰۰۰ تومان
</div>
{/* <img className='bg-white rounded-[11px]' src={wallet} /> */}
</div>
{/* <img src={bell} /> */}
<button className='flex items-center justify-between' onClick={toggleProfileDropState}>
{/* <img className='rounded-full min-w-8 w-8' src={avatar} /> */}
<div className='pe-6 hidden xl:inline-flex' >
<div className='ps-2 pe-1 text-xs text-nowrap'>مهرداد مظفری</div>
{/* <img src={arrow} /> */}
</div>
</button>
</div>
{/* <Dropdown state={profileDrop} toggle={setProfileDrop} /> */}
</div>
</div>
)
}
export default TopBar;
@@ -0,0 +1,62 @@
'use client';
import React, { useState } from 'react'
import TopBar from '../topbar/TopBar'
import { Menu } from '../menu/Menu'
import BottomNavBar from '../navigation/BottomNavBar'
type Props = {} & React.AllHTMLAttributes<HTMLBodyElementEventMap>
function ClientMenuRouteWrapper({ children }: Props) {
const [profileDrop, setProfileDrop] = useState(false);
const [nightMode, setNightMode] = useState(false);
const [menuState, setMenuState] = useState(false);
const [searchModal, setSearchModal] = useState(false);
const toggleProfileDrop = () => {
setProfileDrop((state) => !state);
}
const toggleNightMode = () => {
setNightMode((state) => !state);
}
const toggleMenuState = () => {
setMenuState((state) => !state);
}
const toggleSearchModal = () => {
setSearchModal((state) => !state);
}
// const location = useRouter();
// useEffect(() => {
// setMenuState(false)
// }, [location])
return (
<div>
<header>
<TopBar
profileDropState={profileDrop} toggleProfileDropState={toggleProfileDrop}
menuState={menuState} toggleMenuState={toggleMenuState}
searchModalState={searchModal} toggleSearchModalState={toggleSearchModal}
nightModeState={nightMode} toggleNightModeState={toggleNightMode} />
</header>
<main className="h-full pb-0">
{children}
</main>
<aside>
<Menu menuState={menuState} toggleMenuState={toggleMenuState} />
</aside>
<footer>
<BottomNavBar />
</footer>
</div>
)
}
export default ClientMenuRouteWrapper
@@ -0,0 +1,15 @@
'use client'
import React from 'react'
type Props = { } & React.AllHTMLAttributes<HTMLBodyElementEventMap>
function ClientSideWrapper({ children }: Props) {
return (
<div>
{children}
</div>
)
}
export default ClientSideWrapper