Files
homaroma/apps/spa/src/components/MobileDrawer.tsx
T
2026-06-21 07:04:03 -04:00

255 lines
11 KiB
TypeScript

import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import type { MegaMenuPayload, MenuColumn, MenuLink } from '../store/menuStore';
import { X, ChevronRight, ChevronLeft, Globe } from 'lucide-react';
import { useAuthStore } from '../store/authStore';
import { loginLiff, logoutLiff } from '../lib/liff';
import { useTranslation } from 'react-i18next';
interface MobileDrawerProps {
isOpen: boolean;
onClose: () => void;
items: MegaMenuPayload[];
onOpenLang: () => void;
}
const getLocalizedText = (textObj: Record<string, string> | string | undefined, lang: string) => {
if (typeof textObj === 'string') return textObj;
if (!textObj) return '';
return textObj[lang] || textObj['en'] || Object.values(textObj)[0] || '';
};
type MenuNode =
| { type: 'root'; data: MegaMenuPayload[]; title: string | Record<string, string> }
| { type: 'mega'; data: MenuColumn[]; title: string | Record<string, string> }
| { type: 'links'; data: MenuLink[]; title: string | Record<string, string> };
export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawerProps) {
const { customer } = useAuthStore();
const navigate = useNavigate();
const { i18n } = useTranslation();
const lang = i18n.language ? i18n.language.substring(0, 2) : 'en';
const [stack, setStack] = useState<MenuNode[]>([{ type: 'root', data: items, title: 'Menu' }]);
useEffect(() => {
if (!isOpen) {
setTimeout(() => {
setStack([{ type: 'root', data: items, title: 'Menu' }]);
}, 300);
} else {
if (stack.length === 1 && stack[0].data !== items) {
setStack([{ type: 'root', data: items, title: 'Menu' }]);
}
}
}, [isOpen, items]);
const handleLinkClick = (url: string) => {
navigate(url);
onClose();
};
const pushView = (node: MenuNode) => {
setStack((prev) => [...prev, node]);
};
const popView = () => {
setStack((prev) => (prev.length > 1 ? prev.slice(0, -1) : prev));
};
const renderView = (view: MenuNode) => {
if (view.type === 'root') {
return (
<div className="px-6 py-2 space-y-1">
{view.data.map((item, index) => (
<div key={item.id || `menu-root-${index}`} className="border-b border-pop-black/10 last:border-0">
{item.columns && item.columns.length > 0 ? (
<button
onClick={() => pushView({ type: 'mega', data: item.columns!, title: item.label })}
className="w-full flex items-center justify-between py-4 text-sm font-medium tracking-wide text-pop-black/70 hover:text-pop-black uppercase group"
>
<span>{getLocalizedText(item.label, lang)}</span>
<ChevronRight className="w-4 h-4 text-pop-black/40 group-hover:text-pop-black transition-colors" />
</button>
) : (
<button
onClick={() => handleLinkClick(item.url || '#')}
className="w-full text-left py-4 text-sm font-medium tracking-wide text-pop-black/70 hover:text-pop-black uppercase"
>
{getLocalizedText(item.label, lang)}
</button>
)}
</div>
))}
{customer && (
<div className="border-b border-pop-black/10">
<button
onClick={() => handleLinkClick('/profile')}
className="w-full text-left py-4 text-sm font-medium tracking-wide text-pop-black/70 hover:text-pop-black uppercase"
>
👤 My Profile
</button>
</div>
)}
</div>
);
}
if (view.type === 'mega') {
return (
<div className="px-6 py-4">
{view.data.map((col, idx) => (
<div key={idx} className="mb-8 last:mb-0">
<h4 className="text-[10px] uppercase tracking-widest text-pop-black/60 font-bold mb-4">
{getLocalizedText(col.title, lang)}
</h4>
<div className="space-y-2">
{col.links.map((link, lIdx) => (
<div key={lIdx}>
{link.links && link.links.length > 0 ? (
<button
onClick={() => pushView({ type: 'links', data: link.links!, title: link.label })}
className="w-full flex items-center justify-between py-2 text-xs uppercase tracking-widest text-pop-black/70 hover:text-pop-black group"
>
<span>{getLocalizedText(link.label, lang)}</span>
<ChevronRight className="w-4 h-4 text-pop-black/40 group-hover:text-pop-black transition-colors" />
</button>
) : (
<button
onClick={() => handleLinkClick(link.url || '#')}
className="w-full text-left py-2 text-xs uppercase tracking-widest text-pop-black/70 hover:text-pop-black"
>
{getLocalizedText(link.label, lang)}
</button>
)}
</div>
))}
</div>
</div>
))}
</div>
);
}
if (view.type === 'links') {
return (
<div className="px-6 py-4 space-y-2">
{view.data.map((link, lIdx) => (
<div key={lIdx}>
{link.links && link.links.length > 0 ? (
<button
onClick={() => pushView({ type: 'links', data: link.links!, title: link.label })}
className="w-full flex items-center justify-between py-2 text-xs uppercase tracking-widest text-pop-black/70 hover:text-pop-black group"
>
<span>{getLocalizedText(link.label, lang)}</span>
<ChevronRight className="w-4 h-4 text-pop-black/40 group-hover:text-pop-black transition-colors" />
</button>
) : (
<button
onClick={() => handleLinkClick(link.url || '#')}
className="w-full text-left py-2 text-xs uppercase tracking-widest text-pop-black/70 hover:text-pop-black"
>
{getLocalizedText(link.label, lang)}
</button>
)}
</div>
))}
</div>
);
}
return null;
};
return (
<div className={`fixed inset-0 z-[60] md:hidden transition-all duration-300 ${isOpen ? 'visible' : 'invisible'}`}>
<div
className={`absolute inset-0 bg-pop-black/40 backdrop-blur-sm transition-opacity duration-300 ${isOpen ? 'opacity-100' : 'opacity-0'}`}
onClick={onClose}
/>
<div
className={`absolute top-0 right-0 h-full w-80 max-w-[85vw] bg-pop-bg shadow-2xl flex flex-col transition-transform duration-300 ${isOpen ? 'translate-x-0' : 'translate-x-full'}`}
>
{/* Static Top Header */}
<div className="flex items-center justify-between px-6 py-5 border-b border-pop-black/10 bg-pop-bg z-10 shrink-0">
<span className="text-sm font-bold uppercase tracking-widest text-pop-black">Menu</span>
<div className="flex items-center gap-3">
<button onClick={onOpenLang} className="px-2.5 py-1 rounded-md text-[10px] font-bold text-pop-black border border-pop-black/20 uppercase bg-pop-black/5 flex items-center gap-1 shrink-0">
<Globe className="w-3 h-3" />
{i18n.language.substring(0, 2).toUpperCase()}
</button>
<button onClick={onClose} className="p-1.5 rounded-full hover:bg-pop-black/5 transition-all text-pop-black/70">
<X className="w-5 h-5" />
</button>
</div>
</div>
{/* Sliding Views Container */}
<div className="flex-1 relative overflow-hidden bg-pop-bg">
{stack.map((view, index) => {
const isCurrent = index === stack.length - 1;
const isPast = index < stack.length - 1;
return (
<div
key={index}
className={`absolute inset-0 flex flex-col bg-pop-bg transition-transform duration-300 ease-in-out ${
isCurrent ? 'translate-x-0' : isPast ? '-translate-x-full' : 'translate-x-full'
}`}
>
{/* Back Button for nested views */}
{index > 0 && (
<button
onClick={popView}
className="flex items-center px-6 py-4 text-xs font-bold uppercase tracking-widest text-pop-black border-b border-pop-black/5 hover:bg-pop-black/5 transition-colors shrink-0"
>
<ChevronLeft className="w-4 h-4 mr-2" />
Back to {getLocalizedText(stack[index - 1].title, lang)}
</button>
)}
{/* Optional View Title Header for nested views to emphasize context */}
{index > 0 && (
<div className="px-6 py-5 shrink-0">
<h3 className="text-lg font-bold uppercase tracking-widest text-pop-black">{getLocalizedText(view.title, lang)}</h3>
</div>
)}
{/* View Content */}
<div className="flex-1 overflow-y-auto pb-6">
{renderView(view)}
</div>
</div>
);
})}
</div>
{/* Static Bottom Footer */}
<div className="px-6 pb-8 pt-6 border-t border-pop-black/10 bg-pop-bg z-10 shrink-0 space-y-3">
<button
onClick={() => handleLinkClick('/shop')}
className="block w-full py-3.5 bg-pop-black text-pop-bg font-bold text-center rounded-xl text-xs uppercase tracking-widest hover:bg-pop-black/80 transition-colors duration-300 shadow-sm"
>
Shop Now
</button>
{customer ? (
<button
onClick={() => { logoutLiff(); onClose(); }}
className="block w-full py-3.5 text-center rounded-xl text-xs font-bold uppercase tracking-widest text-rose-400 hover:bg-rose-50 transition-colors border border-rose-500/20"
>
Logout
</button>
) : (
<button
onClick={() => { loginLiff(); onClose(); }}
className="block w-full py-3.5 text-center rounded-xl text-xs font-bold uppercase tracking-widest text-[#00B900] hover:bg-[#00B900]/10 transition-colors border border-[#00B900]/50"
>
Login with LINE
</button>
)}
</div>
</div>
</div>
);
}