copy base dmenu to dkala

This commit is contained in:
hamid zarghami
2026-02-07 15:31:22 +03:30
commit c9e37f6177
521 changed files with 57786 additions and 0 deletions
+451
View File
@@ -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;
+29
View File
@@ -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>
)
}