465 lines
21 KiB
TypeScript
465 lines
21 KiB
TypeScript
'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, toastLoginRequired } 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;
|
||
|
||
const isIOSStandalone =
|
||
('standalone' in window.navigator) &&
|
||
(window.navigator as { standalone?: boolean }).standalone === true;
|
||
|
||
const isDisplayModeStandalone =
|
||
window.matchMedia('(display-mode: standalone)').matches ||
|
||
window.matchMedia('(display-mode: fullscreen)').matches;
|
||
|
||
return isIOSStandalone || isDisplayModeStandalone;
|
||
};
|
||
|
||
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);
|
||
|
||
const handleBeforeInstallPrompt = (e: Event) => {
|
||
e.preventDefault();
|
||
setDeferredPrompt(e as BeforeInstallPromptEvent);
|
||
};
|
||
|
||
const handleAppInstalled = () => {
|
||
setDeferredPrompt(null);
|
||
setIsPWAInstalled(true);
|
||
};
|
||
|
||
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||
window.addEventListener('appinstalled', handleAppInstalled);
|
||
|
||
return () => {
|
||
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||
window.removeEventListener('appinstalled', handleAppInstalled);
|
||
};
|
||
}, []);
|
||
|
||
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) {
|
||
toastLoginRequired(() => router.push(`/${name}/auth`), '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; |