copy base dmenu to dkala
This commit is contained in:
@@ -0,0 +1,451 @@
|
||||
'use client';
|
||||
|
||||
import React, { useMemo, useRef, useState, useEffect } from 'react';
|
||||
import { motion, Variants } from 'framer-motion';
|
||||
import SideMenuItem from './SideMenuItem';
|
||||
import NoteBoardIcon from '../icons/NoteBoardIcon';
|
||||
import BlurredOverlayContainer from '../overlays/BlurredOverlayContainer';
|
||||
import NightModeSwitch from '../button/NightModeSwitch';
|
||||
import Button from '../button/PrimaryButton';
|
||||
import { useParams, usePathname, useRouter } from 'next/navigation';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CalendarSearch, Cup, DirectboxReceive, DirectInbox, DocumentCopy, Game, Icon, Instagram, Like1, Login, LogoutCurve, Notification, Receipt1, Setting2, Share, TicketDiscount, Whatsapp } from 'iconsax-react';
|
||||
import TelegramIcon from '../icons/TelegramIcon';
|
||||
import Modal from '../utils/Modal';
|
||||
import LogoutPrompt from '@/features/general/LogoutPrompt';
|
||||
import useToggle from '@/hooks/helpers/useToggle';
|
||||
import { useGetAbout } from '@/app/[name]/(Main)/about/hooks/useAboutData';
|
||||
import Image from 'next/image';
|
||||
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
||||
import { toast } from '../Toast';
|
||||
|
||||
type MenuItemType = {
|
||||
href: string | undefined;
|
||||
title: string;
|
||||
icon: Icon;
|
||||
auth?: boolean;
|
||||
guestOnly?: boolean;
|
||||
premiumOnly?: boolean;
|
||||
};
|
||||
|
||||
const menuItems: Array<Array<MenuItemType>> = [
|
||||
[
|
||||
{ href: 'notifications', title: 'Notifications', icon: Notification },
|
||||
{ href: 'transactions/discount/club', title: 'CustomerClub', icon: Cup, premiumOnly: true },
|
||||
{ href: 'transactions/discount', title: 'Discounts', icon: TicketDiscount, premiumOnly: true },
|
||||
{ href: 'order/history', title: 'Orders', icon: CalendarSearch, premiumOnly: true },
|
||||
{ href: 'transactions', title: 'Transactions', icon: Receipt1, premiumOnly: true },
|
||||
{ href: 'game', title: 'Games', icon: Game },
|
||||
{ href: '?share', title: 'ShareWithFriends', icon: Like1 },
|
||||
{ href: 'report', title: 'ReportProblem', icon: NoteBoardIcon },
|
||||
{ href: '?installpwa', title: 'InstallApp', icon: DirectboxReceive }
|
||||
],
|
||||
[],
|
||||
[
|
||||
{ auth: true, href: 'profile/settings', title: 'Preferences', icon: Setting2 },
|
||||
{ auth: true, href: '?logout', title: 'Logout', icon: LogoutCurve },
|
||||
{ guestOnly: true, href: 'auth', title: 'LoginSignup', icon: Login }
|
||||
]
|
||||
];
|
||||
|
||||
type Props = {
|
||||
menuState: boolean;
|
||||
toggleMenuState: React.MouseEventHandler<HTMLDivElement> | undefined;
|
||||
nightModeState: boolean;
|
||||
togglenightModeState: React.MouseEventHandler<HTMLDivElement> | undefined;
|
||||
};
|
||||
|
||||
type BeforeInstallPromptEvent = Event & {
|
||||
prompt: () => Promise<void>;
|
||||
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
|
||||
};
|
||||
|
||||
const isIOS = () => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return /iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
||||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
||||
};
|
||||
|
||||
const isStandalone = () => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return ('standalone' in window.navigator) && (window.navigator as { standalone?: boolean }).standalone === true;
|
||||
};
|
||||
|
||||
function SideMenu({ menuState, toggleMenuState, nightModeState, togglenightModeState }: Props) {
|
||||
|
||||
const menuStateMemo = useMemo(() => menuState, [menuState]);
|
||||
const closeRef = useRef<HTMLDivElement>(null);
|
||||
const { state: logoutModal, toggle: toggleLogoutModal } = useToggle();
|
||||
const { state: shareModal, toggle: toggleShareModal } = useToggle();
|
||||
const { state: installPwaModal, toggle: toggleInstallPwaModal } = useToggle();
|
||||
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
|
||||
const [isIOSDevice, setIsIOSDevice] = useState(false);
|
||||
const [isPWAInstalled, setIsPWAInstalled] = useState(false);
|
||||
const { data: aboutData } = useGetAbout();
|
||||
const { data: profile, isSuccess } = useGetProfile();
|
||||
const profileData = profile?.data;
|
||||
const isLoggedIn = isSuccess && profileData;
|
||||
const isPremium = aboutData?.data?.plan === 'premium';
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
const params = useParams();
|
||||
const { name } = params;
|
||||
const pathname = usePathname();
|
||||
const { t: tMenu } = useTranslation('common', {
|
||||
keyPrefix: 'SideMenu'
|
||||
});
|
||||
const { t: tShareModal } = useTranslation('common', {
|
||||
keyPrefix: 'ShareModal'
|
||||
});
|
||||
const { t: tInstallPwaModal } = useTranslation('common', {
|
||||
keyPrefix: 'InstallPwaModal'
|
||||
});
|
||||
|
||||
const variants: Variants = {
|
||||
hidden: { x: '100%', transition: { duration: 0.1, ease: 'easeInOut' } },
|
||||
visible: { x: 0, transition: { delay: 0.075, duration: 0.1, ease: 'easeInOut' } }
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const ios = isIOS();
|
||||
const standalone = isStandalone();
|
||||
setIsIOSDevice(ios);
|
||||
setIsPWAInstalled(standalone);
|
||||
|
||||
if (!ios) {
|
||||
const handleBeforeInstallPrompt = (e: Event) => {
|
||||
e.preventDefault();
|
||||
setDeferredPrompt(e as BeforeInstallPromptEvent);
|
||||
};
|
||||
|
||||
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleInstallPWA = async () => {
|
||||
if (!deferredPrompt) {
|
||||
return;
|
||||
}
|
||||
|
||||
deferredPrompt.prompt();
|
||||
const { outcome } = await deferredPrompt.userChoice;
|
||||
|
||||
if (outcome === 'accepted') {
|
||||
setDeferredPrompt(null);
|
||||
toggleInstallPwaModal();
|
||||
}
|
||||
};
|
||||
|
||||
const getShareUrl = () => {
|
||||
if (typeof window === 'undefined') return '';
|
||||
return window.location.href;
|
||||
};
|
||||
|
||||
const copyToClipboard = async (showToast: boolean = true) => {
|
||||
try {
|
||||
const url = getShareUrl();
|
||||
await navigator.clipboard.writeText(url);
|
||||
if (showToast) {
|
||||
toast('لینک با موفقیت کپی شد', 'success');
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
if (showToast) {
|
||||
toast('خطا در کپی کردن لینک', 'error');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
await copyToClipboard();
|
||||
toggleShareModal();
|
||||
};
|
||||
|
||||
const handleShareTelegram = () => {
|
||||
const url = encodeURIComponent(getShareUrl());
|
||||
window.open(`https://t.me/share/url?url=${url}`, '_blank');
|
||||
};
|
||||
|
||||
const handleShareWhatsApp = () => {
|
||||
const url = encodeURIComponent(getShareUrl());
|
||||
const text = encodeURIComponent('اپلیکیشن ما را ببینید!');
|
||||
window.open(`https://wa.me/?text=${text}%20${url}`, '_blank');
|
||||
};
|
||||
|
||||
const handleShareInstagram = async () => {
|
||||
const success = await copyToClipboard(false);
|
||||
if (success) {
|
||||
toast('لینک کپی شد. لطفاً آن را در اینستاگرام به اشتراک بگذارید', 'info');
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotificationClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
if (!isLoggedIn) {
|
||||
toast('ابتدا لاگین کنید', 'info');
|
||||
} else {
|
||||
router.push(`/${name}/notifications`);
|
||||
}
|
||||
};
|
||||
|
||||
const hrefOnClicks = [
|
||||
{ href: '?logout', handler: toggleLogoutModal },
|
||||
{ href: '?share', handler: toggleShareModal },
|
||||
{ href: '?installpwa', handler: toggleInstallPwaModal },
|
||||
{ href: 'notifications', handler: handleNotificationClick }
|
||||
];
|
||||
|
||||
const renderMenu = () => {
|
||||
return (
|
||||
<section className="flex-1 overflow-y-scroll noscrollbar pt-6 pb-8 flex flex-col justify-between md:w-[200px] xl:w-[250px]">
|
||||
{
|
||||
aboutData?.data?.logo && (
|
||||
<div >
|
||||
<Image className='mx-auto' src={aboutData?.data?.logo} alt='logo' width={110} height={100} unoptimized />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className='mt-4'>
|
||||
<header className="px-5 md:px-6 xl:px-12">
|
||||
<h6 className="text-start font-bold text-sm text-menu-header mt-2 mb-[19px] dark:text-disabled-text">منو</h6>
|
||||
</header>
|
||||
|
||||
<nav aria-label={tMenu('NavAriaLabel')}>
|
||||
<ul>
|
||||
{menuItems[0]
|
||||
.filter(item => {
|
||||
if (item.auth && !isSuccess) return false;
|
||||
if (item.guestOnly && isSuccess) return false;
|
||||
if (item.href === '?installpwa' && isPWAInstalled) return false;
|
||||
if (item.premiumOnly && !isPremium) return false;
|
||||
return true;
|
||||
})
|
||||
.map(({ icon: Icon, ...item }, index) => {
|
||||
const href = `/${name}/${item.href}`;
|
||||
const isActive = pathname === href;
|
||||
return (
|
||||
<li key={index} className="mt-4 h-6">
|
||||
<SideMenuItem
|
||||
className={clsx(isActive && 'text-primary! border-primary! dark:text-neutral-200! dark:border-neutral-200!', 'border-r-4!')}
|
||||
href={href ?? ''}
|
||||
title={tMenu(item.title)}
|
||||
onClick={
|
||||
typeof item.href === 'string'
|
||||
? hrefOnClicks.find((i) => i.href === item.href)?.handler
|
||||
: undefined
|
||||
}
|
||||
icon={
|
||||
<Icon
|
||||
variant={isActive ? 'Bold' : 'Linear'}
|
||||
className={clsx(
|
||||
isActive ? 'text-primary fill-primary dark:text-neutral-200 dark:fill-neutral-200' : 'stroke-icon-deactive text-icon-deactive'
|
||||
)}
|
||||
size={20} width={20} height={20} />
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<section aria-label={tMenu('ControlsAriaLabel')}>
|
||||
<ul className="flex flex-col pt-16">
|
||||
{menuItems[2]
|
||||
.filter(item => {
|
||||
if (item.auth && !isSuccess) return false;
|
||||
if (item.guestOnly && isSuccess) return false;
|
||||
return true;
|
||||
})
|
||||
.map(({ icon: Icon, ...item }, index) => {
|
||||
const href = `/${name}/${item.href}`;
|
||||
const isActive = pathname === href;
|
||||
return (
|
||||
<li key={index} className="mt-4 h-6 w-full">
|
||||
<SideMenuItem
|
||||
key={index}
|
||||
href={href ?? ''}
|
||||
className={clsx(isActive && 'text-primary! border-primary! dark:text-neutral-200! dark:border-neutral-200!', 'border-r-4!')}
|
||||
onClick={
|
||||
typeof item.href === 'string'
|
||||
? hrefOnClicks.find((i) => i.href === item.href)?.handler
|
||||
: undefined
|
||||
}
|
||||
title={tMenu(item.title)}
|
||||
icon={
|
||||
<Icon
|
||||
variant={isActive ? 'Bold' : 'Linear'}
|
||||
className={clsx(
|
||||
isActive ? 'text-primary fill-primary dark:text-neutral-200 dark:fill-neutral-200' : 'stroke-icon-deactive text-icon-deactive'
|
||||
)}
|
||||
size={20} width={20} height={20} />
|
||||
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
<li className="mt-4 h-6">
|
||||
<SideMenuItem
|
||||
className='pr-7'
|
||||
href={''}
|
||||
title={''}
|
||||
icon={<></>}>
|
||||
<NightModeSwitch
|
||||
checked={nightModeState}
|
||||
onClick={togglenightModeState}
|
||||
/>
|
||||
</SideMenuItem>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='w-full' ref={closeRef} onClick={toggleMenuState}>
|
||||
<aside aria-label={tMenu('AriaLabel')}>
|
||||
<BlurredOverlayContainer className='xl:hidden!' bgOpacity={40} visible={menuStateMemo} outDelay={150} inDuration={200}>
|
||||
<motion.nav
|
||||
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
||||
initial="hidden"
|
||||
animate={menuState ? 'visible' : 'hidden'}
|
||||
variants={variants}
|
||||
data-visible={menuState}
|
||||
data-isvisible={menuStateMemo}
|
||||
onClick={(e) => { e.stopPropagation(); }}
|
||||
className="fixed top-0 bottom-0 right-0 bg-container w-[200px] h-dvh flex flex-col z-40 overflow-clip not-xl:rounded-s-none!"
|
||||
>
|
||||
{renderMenu()}
|
||||
</motion.nav>
|
||||
</BlurredOverlayContainer>
|
||||
<nav
|
||||
className="hidden fixed top-4 bottom-4 right-4 bg-container xl:w-[250px] xl:flex flex-col z-40 overflow-clip xl:rounded-4xl"
|
||||
>
|
||||
{renderMenu()}
|
||||
</nav>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{/* Logout Modal */}
|
||||
<LogoutPrompt
|
||||
visible={logoutModal}
|
||||
onClick={toggleLogoutModal}
|
||||
slug={name as string}
|
||||
/>
|
||||
|
||||
{/* Share modal */}
|
||||
<Modal
|
||||
visible={shareModal}
|
||||
onClick={toggleShareModal}
|
||||
>
|
||||
<h2 className="text-base font-medium">
|
||||
<Share className="inline-block ml-2 mb-1.5 stroke-primary dark:stroke-foreground" size={24} />
|
||||
{tShareModal('Heading')}
|
||||
</h2>
|
||||
<p className="text-xs font-medium mt-6">
|
||||
{tShareModal('Description')}
|
||||
</p>
|
||||
|
||||
<Button onClick={handleCopyLink} className="text-sm mt-8">
|
||||
<DocumentCopy className="inline-block ml-2 mb-0.5 stroke-container dark:stroke-foreground" size={20} />
|
||||
<span className="font-light">{tShareModal('ButtonCopy')}</span>
|
||||
</Button>
|
||||
|
||||
<div className="grid grid-cols-3 gap-5.5 mt-6 px-12">
|
||||
<button
|
||||
onClick={handleShareTelegram}
|
||||
className="flex flex-col items-center cursor-pointer hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<TelegramIcon width={24} height={24} className="mb-1.5 text-primary dark:text-foreground" />
|
||||
<span className='text-xs2 font-medium'>Telegram</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleShareWhatsApp}
|
||||
className="flex flex-col items-center cursor-pointer hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<Whatsapp size={24} className="mb-1.5 stroke-primary dark:stroke-foreground" />
|
||||
<span className='text-xs2 font-medium'>WhatsApp</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleShareInstagram}
|
||||
className="flex flex-col items-center cursor-pointer hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<Instagram size={24} className="mb-1.5 stroke-primary dark:stroke-foreground" />
|
||||
<span className='text-xs2 font-medium'>Instagram</span>
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Install PWA modal */}
|
||||
<Modal
|
||||
visible={installPwaModal}
|
||||
onClick={toggleInstallPwaModal}
|
||||
>
|
||||
<h2 className="text-base font-medium">
|
||||
<DirectInbox className="inline-block ml-2 mb-1.5 stroke-primary dark:stroke-foreground" size={24} />
|
||||
{tInstallPwaModal('Heading')}
|
||||
</h2>
|
||||
{isPWAInstalled ? (
|
||||
<p className="mt-6 text-xs font-medium text-center text-foreground/80 dark:text-neutral-200">
|
||||
{tInstallPwaModal('AlreadyInstalled')}
|
||||
</p>
|
||||
) : isIOSDevice ? (
|
||||
<div className="mt-5 rounded-3xl bg-primary/5 px-4 py-5 backdrop-blur-sm dark:bg-white/5">
|
||||
<p className="text-xs font-medium leading-relaxed text-center text-foreground/80 dark:text-neutral-200">
|
||||
{tInstallPwaModal('IOSDescription')}
|
||||
</p>
|
||||
<ol className="mt-5 space-y-3 text-xs font-medium rtl:text-right">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary/10 text-[11px] font-bold text-primary">
|
||||
۱
|
||||
</span>
|
||||
<span>{tInstallPwaModal('IOSStep1')}</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary/10 text-[11px] font-bold text-primary">
|
||||
۲
|
||||
</span>
|
||||
<span>{tInstallPwaModal('IOSStep2')}</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary/10 text-[11px] font-bold text-primary">
|
||||
۳
|
||||
</span>
|
||||
<span>{tInstallPwaModal('IOSStep3')}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="mt-6 text-xs font-medium leading-relaxed text-center text-foreground/80 dark:text-neutral-200">
|
||||
{tInstallPwaModal('Description')}
|
||||
</p>
|
||||
{deferredPrompt ? (
|
||||
<Button onClick={handleInstallPWA} className="mt-8 text-sm">
|
||||
<span className="font-light">{tInstallPwaModal('ButtonOk')}</span>
|
||||
</Button>
|
||||
) : (
|
||||
<p className="mt-4 text-[11px] font-medium leading-relaxed text-center text-foreground/60 dark:text-neutral-300">
|
||||
{tInstallPwaModal('ChromeFallback')}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SideMenu;
|
||||
@@ -0,0 +1,29 @@
|
||||
'use client';
|
||||
|
||||
import Link, { LinkProps } from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
icon: React.ReactElement
|
||||
title: string
|
||||
} & LinkProps & React.AnchorHTMLAttributes<HTMLAnchorElement>
|
||||
|
||||
export default function SideMenuItem({ href, title, icon, className, children, ...restProps }: Props) {
|
||||
return (
|
||||
<Link
|
||||
{...restProps}
|
||||
href={href}
|
||||
className={`
|
||||
inline-flex gap-3 ps-6 xl:ps-9 h-full w-full items-center overflow-hidden text-nowrap
|
||||
text-xs font-light text-disabled-text border-transparent
|
||||
data-active:border-s-6 data-active:border-primary data-active:text-primary
|
||||
dark:data-active:border-foreground dark:data-active:text-foreground
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
<span className="shrink-0">{icon}</span>
|
||||
<span className='mt-0.5'>{title}</span>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user