add: side menu transition
This commit is contained in:
+7
-13
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user