Files
dkala-front/src/components/menu/SideMenu.tsx
T
hamid zarghami 2fc546fd81 history list
2026-02-10 15:34:02 +03:30

451 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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: false },
{ href: 'transactions/discount', title: 'Discounts', icon: TicketDiscount, premiumOnly: false },
{ href: 'order/history', title: 'Orders', icon: CalendarSearch, premiumOnly: false },
{ href: 'transactions', title: 'Transactions', icon: Receipt1, premiumOnly: false },
{ 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;