canina/frontend/application/components/Header.tsx

513 lines
26 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, { useState, useRef, useEffect } from "react";
import { Search, ChevronDown, Menu, X, Pill, ShieldCheck, HeartPulse, Sparkles, ShoppingBag, User, PlusCircle, Check, Building2, LogIn, Wallet, LogOut, MapPin, FileHeart, Dog, Cat } from "lucide-react";
import { motion, AnimatePresence } from "motion/react";
import Link from "next/link";
import { useCartStore } from "../lib/store/cartStore";
import { usePetStore } from "../lib/store/usePetStore";
import { useUserStore } from "../lib/store/userStore";
import { useSettingsStore } from "../lib/store/settingsStore";
import { toast } from "sonner";
import HeaderButton from "./HeaderButton";
import AuthModal from "./AuthModal";
import { cn } from "../lib/utils";
import { productService } from "../lib/services/productService";
const MENU_ICONS: Record<string, React.ReactNode> = {
joints: <Pill className="w-5 h-5" />,
immune: <ShieldCheck className="w-5 h-5" />,
energy: <HeartPulse className="w-5 h-5" />,
"special-care": <Sparkles className="w-5 h-5" />,
};
export default function Header({
onNavigate,
onShopNavigate,
currentView,
onCartOpen,
onSearch,
onB2BOpen
}: {
onNavigate: (v: any) => void,
onShopNavigate: (c?: string, s?: string) => void,
currentView: string,
onCartOpen: () => void,
onSearch: (q: string) => void,
onB2BOpen: () => void
}) {
const [isMegaMenuOpen, setIsMegaMenuOpen] = useState(false);
const [isSearchFocused, setIsSearchFocused] = useState(false);
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
const [isPetSwitcherOpen, setIsPetSwitcherOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isMobileSolutionsOpen, setIsMobileSolutionsOpen] = useState(false);
const [menuItems, setMenuItems] = useState<{
id: string;
title: string;
icon: React.ReactNode;
solutions: string[];
}[]>([]);
const petMenuRef = useRef<HTMLDivElement>(null);
const { getTotalItems } = useCartStore();
const { pets, activePetId, setActivePet, getActivePet } = usePetStore();
const { role, isLoggedIn, logout, profile } = useUserStore();
const activePet = getActivePet();
const getText = useSettingsStore(state => state.getText);
useEffect(() => {
const loadNavFilters = async () => {
try {
const data = await productService.getNavigationFilters();
const mapped = data.map(item => ({
id: item.slug,
title: item.name,
icon: MENU_ICONS[item.slug] || <Pill className="w-5 h-5" />,
solutions: item.symptoms
}));
setMenuItems(mapped);
} catch (e) {
console.error("Failed to load navigation filters:", e);
}
};
loadNavFilters();
}, []);
// Click Outside logic
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (petMenuRef.current && !petMenuRef.current.contains(event.target as Node)) {
setIsPetSwitcherOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
if (searchQuery.trim()) {
onSearch(searchQuery);
setIsSearchFocused(false);
}
};
return (
<>
<AuthModal isOpen={isAuthModalOpen} onClose={() => setIsAuthModalOpen(false)} />
<div className="bg-canina-gold text-canina-dark text-xs font-black py-2.5 text-center font-vazir tracking-wider flex items-center justify-center gap-2">
<span>🚚</span>
<span>{getText('shipping_notice', "ارسال رایگان برای سفارش‌های بالای ۱۵۰ هزار تومان")}</span>
</div>
<header className="sticky top-0 z-50 bg-white/95 backdrop-blur-md border-b border-medical-gray-100 shadow-sm">
<div className="max-w-7xl mx-auto h-24 flex items-center justify-between gap-4 px-4 sm:px-6 lg:px-8" dir="rtl">
<div className="flex items-center gap-2">
{/* Mobile Menu Toggle */}
<button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="lg:hidden p-2 rounded-xl text-medical-gray-600 hover:bg-medical-gray-50 transition-all"
aria-label="منو"
>
{isMobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
{/* Logo Section */}
<Link
href="/"
className="flex-shrink-0 flex items-center gap-3 cursor-pointer group"
onClick={() => setIsMobileMenuOpen(false)}
>
<div className="w-12 h-12 bg-canina-blue rounded-2xl flex items-center justify-center text-white font-bold text-2xl group-hover:bg-medical-gray-900 transition-all shadow-xl shadow-canina-blue/20 italic">C</div>
<div className="flex flex-col justify-center gap-1">
<span className="text-canina-blue font-black text-2xl tracking-tighter italic font-sans flex items-center gap-2 leading-none">
Canina
<span className="text-sm not-italic font-medium border-r-2 border-medical-gray-200 pr-2 font-vazir">
{getText('brand_name_fa', "ایران")}
</span>
</span>
<span className="text-[9px] text-medical-gray-400 font-bold uppercase tracking-wider font-vazir leading-none">
{getText('brand_subtitle', "نماینده رسمی Canina Pharma GmbH آلمان")}
</span>
</div>
</Link>
</div>
{/* Navigation Items */}
<nav className="hidden lg:flex items-center gap-1 flex-grow justify-center h-full">
<div
className="relative h-full flex items-center group"
onMouseEnter={() => setIsMegaMenuOpen(true)}
onMouseLeave={() => setIsMegaMenuOpen(false)}
>
<button className={`flex items-center justify-center gap-1.5 px-4 py-3 rounded-xl text-sm font-black transition-all whitespace-nowrap focus-visible:ring-4 focus-visible:ring-canina-blue/30 focus-visible:outline-none min-h-[48px] ${isMegaMenuOpen ? 'bg-medical-gray-50 text-canina-blue' : 'text-medical-gray-600 hover:bg-medical-gray-50 hover:text-canina-blue'} font-vazir`}>
{getText('nav_solutions', "دسته بندی درمانی")}
<ChevronDown className={`w-4 h-4 transition-transform duration-300 ${isMegaMenuOpen ? 'rotate-180' : ''}`} />
</button>
<AnimatePresence>
{isMegaMenuOpen && (
<motion.div
initial={{ opacity: 0, y: 15, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 15, scale: 0.98 }}
className="absolute top-[80%] right-0 w-[640px] bg-white border border-medical-gray-100 shadow-2xl rounded-[2.5rem] p-10 grid grid-cols-2 gap-10 z-50 pointer-events-auto"
>
{menuItems.map((item, idx) => (
<div key={idx} className="group/item">
<Link
href={`/shop?category=${item.id}`}
onClick={() => setIsMegaMenuOpen(false)}
className="flex items-center gap-4 mb-4 cursor-pointer block"
>
<div className="p-3 bg-medical-gray-50 rounded-2xl text-canina-blue group-hover/item:bg-canina-blue group-hover/item:text-white transition-all shadow-sm">
{item.icon}
</div>
<h4 className="font-black text-medical-gray-900 text-sm font-vazir whitespace-nowrap">{item.title}</h4>
</Link>
<ul className="space-y-2.5 border-r-2 border-medical-gray-50 pr-4">
{item.solutions.map((sol, sIdx) => (
<li key={sIdx}>
<Link
href={`/shop?category=${item.id}&symptoms=${sol}`}
className="text-[12px] text-medical-gray-400 hover:text-canina-blue hover:translate-x-[-4px] transition-all font-bold cursor-pointer font-vazir whitespace-nowrap block"
onClick={() => setIsMegaMenuOpen(false)}
>
{sol}
</Link>
</li>
))}
</ul>
</div>
))}
</motion.div>
)}
</AnimatePresence>
</div>
{[
{ id: 'shop', label: getText('nav_products', 'محصولات تخصصی'), href: '/shop' },
{ id: 'wiki', label: getText('nav_wiki', 'دانشنامه علمی'), href: '/wiki' },
{ id: 'blog', label: getText('nav_blog', 'مجله سلامت پت'), href: '/blog' },
{ id: 'profile', label: getText('nav_pet_profiles', 'شناسنامه پت'), href: '/profile' }
].map((item) => (
<Link
key={item.id}
href={item.href}
className={`px-4 py-3 rounded-xl text-sm font-black transition-all font-vazir whitespace-nowrap focus-visible:ring-4 focus-visible:ring-canina-blue/30 focus-visible:outline-none min-h-[48px] flex items-center justify-center ${currentView === item.id ? 'bg-canina-blue/5 text-canina-blue' : 'text-medical-gray-600 hover:bg-medical-gray-50 hover:text-canina-blue'}`}
>
{item.label}
</Link>
))}
</nav>
{/* Action Area */}
<div className="flex items-center gap-4 flex-shrink-0 justify-end h-16 pl-2">
{/* Elastic Search */}
<div className="relative flex items-center justify-end z-20">
<AnimatePresence>
{isSearchFocused && (
<motion.div
initial={{ width: 0, opacity: 0 }}
animate={{ width: 320, opacity: 1 }}
exit={{ width: 0, opacity: 0 }}
transition={{ type: "spring", damping: 25, stiffness: 220 }}
className="absolute right-0 top-1/2 -translate-y-1/2 bg-white border border-canina-blue/30 border-l rounded-2xl shadow-xl shadow-canina-blue/5 overflow-hidden h-12 flex items-center pr-12 pl-4 z-25"
>
<form onSubmit={handleSearch} className="w-full flex items-center">
<input
autoFocus
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
onBlur={() => !searchQuery && setIsSearchFocused(false)}
placeholder="جستجوی محصولات، مقالات سلامت و غیره…"
dir="rtl"
className="w-full bg-transparent py-2 pr-4 text-[12px] font-black outline-none font-vazir text-medical-gray-900 text-right placeholder:text-medical-gray-400 placeholder:text-right"
/>
<input type="submit" className="hidden" />
</form>
</motion.div>
)}
</AnimatePresence>
<HeaderButton
variant={isSearchFocused ? "primary" : "secondary"}
className={cn(
"w-12 h-12 transition-all duration-300",
isSearchFocused
? "z-30 bg-canina-blue text-white rounded-r-2xl rounded-l-none border border-canina-blue border-l-0 shadow-none"
: "rounded-2xl"
)}
onClick={() => {
if (isSearchFocused && searchQuery.trim()) {
onSearch(searchQuery);
setIsSearchFocused(false);
} else {
setIsSearchFocused(!isSearchFocused);
}
}}
>
<Search className={cn("w-5 h-5", isSearchFocused ? "text-white" : "text-canina-blue")} />
</HeaderButton>
</div>
<div className="flex gap-2 items-center h-12">
{isLoggedIn ? (
<div className="flex items-center h-full gap-2 p-1.5 bg-medical-gray-50/50 border border-medical-gray-100 rounded-3xl">
{/* User Name (Direct link to dashboard) */}
<div
className="flex items-center gap-2.5 px-4 py-2 hover:bg-white rounded-2xl cursor-pointer transition-all border border-transparent hover:border-medical-gray-200"
onClick={() => onNavigate("user-dashboard")}
>
<User className="w-5 h-5 text-canina-blue" />
<span className="text-[14px] font-black font-vazir text-medical-gray-900 leading-none">
{profile.firstName}
</span>
</div>
{/* Separator Line */}
<div className="w-[1.5px] h-5 bg-medical-gray-200" />
{/* Pet Name & Switcher */}
<div className="relative h-full flex items-center" ref={petMenuRef}>
<div
className={cn(
"flex items-center gap-2.5 px-4 py-2 rounded-2xl transition-all cursor-pointer",
isPetSwitcherOpen ? "bg-canina-blue text-white shadow-lg shadow-canina-blue/20" : "text-canina-blue hover:bg-white"
)}
onClick={() => setIsPetSwitcherOpen(!isPetSwitcherOpen)}
>
{activePet?.type === "گربه" ? (
<Cat className={cn("w-5 h-5", isPetSwitcherOpen ? "text-white" : "text-canina-blue/60")} />
) : (
<Dog className={cn("w-5 h-5", isPetSwitcherOpen ? "text-white" : "text-canina-blue/60")} />
)}
<span className={cn("text-[14px] font-black font-vazir leading-none", isPetSwitcherOpen ? "text-white" : "text-canina-blue")}>
{activePet?.name || "بدون پت"}
</span>
<ChevronDown className={cn("w-4 h-4 transition-transform", isPetSwitcherOpen ? "rotate-180" : "")} />
</div>
<AnimatePresence>
{isPetSwitcherOpen && (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }}
className="absolute top-[125%] left-0 w-72 bg-white border border-medical-gray-100 shadow-2xl rounded-[1.5rem] p-5 z-50 pointer-events-auto"
>
<div className="text-[9px] font-black text-medical-gray-400 uppercase tracking-[0.2em] mb-4 pr-1">{getText('nav_pet_switcher_title', "تغییر پت یا مدیریت حساب")}</div>
<div className="space-y-1 mb-4">
{pets.map(pet => (
<div
key={pet.id}
className={cn(
"w-full flex items-center justify-between p-3 rounded-xl transition-all group/item",
activePetId === pet.id ? "bg-canina-blue/5 text-canina-blue" : "hover:bg-medical-gray-50 text-medical-gray-600"
)}
>
<div
className="flex items-center gap-3 flex-1 cursor-pointer"
onClick={() => {
setActivePet(pet.id);
setIsPetSwitcherOpen(false);
toast.success(`پت فعال به ${pet.name} تغییر یافت`);
}}
>
<div className={cn(
"w-8 h-8 rounded-lg flex items-center justify-center text-[11px] font-black transition-colors",
activePetId === pet.id ? "bg-canina-blue text-white" : "bg-medical-gray-100 text-medical-gray-400 group-hover/item:bg-medical-gray-200"
)}>
{pet.name[0]}
</div>
<span className="text-[13px] font-bold font-vazir">{pet.name}</span>
</div>
<div className="flex items-center gap-1">
{activePetId === pet.id && <Check className="w-4 h-4 ml-2" />}
<button
onClick={(e) => {
e.stopPropagation();
setActivePet(pet.id);
onNavigate({ view: "profile", subview: "detail" });
setIsPetSwitcherOpen(false);
}}
className="p-1.5 rounded-lg hover:bg-canina-blue hover:text-white transition-all text-medical-gray-300 hover:shadow-lg"
title="مشاهده شناسنامه سلامت"
>
<FileHeart className="w-4 h-4" />
</button>
</div>
</div>
))}
</div>
<div className="pt-4 border-t border-medical-gray-100 space-y-1">
<button
onClick={() => { onNavigate({ view: "profile", subview: "add" }); setIsPetSwitcherOpen(false); }}
className="w-full flex items-center gap-3 p-3 text-canina-blue hover:bg-canina-blue/5 rounded-xl transition-all text-[12px] font-black font-vazir"
>
<PlusCircle className="w-5 h-5" />
{getText('nav_register_pet', "ثبت همدم (Pet) جدید")}
</button>
<button
onClick={() => { onNavigate({ view: "user-dashboard" }); setIsPetSwitcherOpen(false); }}
className="w-full flex items-center gap-3 p-3 text-medical-gray-600 hover:bg-medical-gray-50 rounded-xl transition-all text-[12px] font-bold font-vazir"
>
<Wallet className="w-5 h-5" />
{getText('nav_wallet_orders', "کیف پول و سفارشات")}
</button>
<button
onClick={() => { logout(); setIsPetSwitcherOpen(false); }}
className="w-full flex items-center gap-3 p-3 text-red-500 hover:bg-red-50 rounded-xl transition-all text-[12px] font-bold font-vazir mt-1"
>
<LogOut className="w-5 h-5" />
{getText('nav_logout', "خروج از حساب کانینا")}
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
) : (
<HeaderButton
variant="outline"
layout="row"
className="px-6 h-12 text-sm font-black flex items-center justify-center"
onClick={() => setIsAuthModalOpen(true)}
icon={<LogIn className="w-5 h-5 text-canina-blue" />}
>
{getText('nav_login', "ورود / ثبت‌نام")}
</HeaderButton>
)}
<HeaderButton
variant="primary"
className="w-16 h-12"
onClick={onCartOpen}
badge={getTotalItems()}
icon={<ShoppingBag className="w-6 h-6" />}
>
<div className="text-[8px] font-black tracking-tight whitespace-nowrap mt-0.5">{getText('nav_cart', "سبد خرید")}</div>
</HeaderButton>
</div>
</div>
</div>
</header>
{/* Mobile Drawer Menu */}
<AnimatePresence>
{isMobileMenuOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 0.5 }}
exit={{ opacity: 0 }}
onClick={() => setIsMobileMenuOpen(false)}
className="fixed inset-0 bg-black z-40 lg:hidden"
/>
{/* Drawer */}
<motion.div
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ type: "spring", damping: 25, stiffness: 200 }}
className="fixed top-0 right-0 bottom-0 w-80 max-w-[85vw] bg-white z-50 shadow-2xl p-6 flex flex-col gap-6 overflow-y-auto lg:hidden"
dir="rtl"
>
<div className="flex items-center justify-between border-b border-medical-gray-100 pb-4">
<Link href="/" className="flex items-center gap-3" onClick={() => setIsMobileMenuOpen(false)}>
<div className="w-10 h-10 bg-canina-blue rounded-xl flex items-center justify-center text-white font-bold text-xl italic">C</div>
<span className="text-canina-blue font-black text-xl italic font-sans">Canina</span>
</Link>
<button
onClick={() => setIsMobileMenuOpen(false)}
className="p-2 rounded-xl hover:bg-medical-gray-50 text-medical-gray-600 transition-all"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Navigation Links */}
<nav className="flex flex-col gap-2">
{/* Treatment Solutions accordion */}
<div className="flex flex-col">
<button
onClick={() => setIsMobileSolutionsOpen(!isMobileSolutionsOpen)}
className="w-full flex items-center justify-between px-4 py-3 rounded-xl text-sm font-black text-medical-gray-600 hover:bg-medical-gray-50 transition-all font-vazir"
>
<span>{getText('nav_solutions', "دسته بندی درمانی")}</span>
<ChevronDown className={`w-4 h-4 transition-transform duration-300 ${isMobileSolutionsOpen ? 'rotate-180' : ''}`} />
</button>
<AnimatePresence>
{isMobileSolutionsOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="overflow-hidden mr-4 pr-2 border-r-2 border-medical-gray-100 mt-1 space-y-3"
>
{menuItems.map((item, idx) => (
<div key={idx} className="py-1">
<Link
href={`/shop?category=${item.id}`}
onClick={() => setIsMobileMenuOpen(false)}
className="text-[13px] font-bold text-medical-gray-800 font-vazir hover:text-canina-blue cursor-pointer flex items-center gap-2 block"
>
<span className="text-canina-blue/80">{item.icon}</span>
<span>{item.title}</span>
</Link>
<ul className="mt-1 space-y-1 mr-6">
{item.solutions.map((sol, sIdx) => (
<li key={sIdx}>
<Link
href={`/shop?category=${item.id}&symptoms=${sol}`}
className="text-[11px] text-medical-gray-500 hover:text-canina-blue cursor-pointer py-1 font-vazir block"
onClick={() => setIsMobileMenuOpen(false)}
>
{sol}
</Link>
</li>
))}
</ul>
</div>
))}
</motion.div>
)}
</AnimatePresence>
</div>
{[
{ id: 'shop', label: getText('nav_products', 'محصولات تخصصی'), href: '/shop' },
{ id: 'wiki', label: getText('nav_wiki', 'دانشنامه علمی'), href: '/wiki' },
{ id: 'blog', label: getText('nav_blog', 'مجله سلامت پت'), href: '/blog' },
{ id: 'profile', label: getText('nav_pet_profiles', 'شناسنامه پت'), href: '/profile' }
].map((item) => (
<Link
key={item.id}
href={item.href}
onClick={() => setIsMobileMenuOpen(false)}
className={`w-full block text-right px-4 py-3 rounded-xl text-sm font-black transition-all font-vazir ${currentView === item.id
? 'bg-canina-blue/5 text-canina-blue'
: 'text-medical-gray-600 hover:bg-medical-gray-50'
}`}
>
{item.label}
</Link>
))}
</nav>
</motion.div>
</>
)}
</AnimatePresence>
</>
);
}