canina/frontend/application/components/Header.tsx
parsa aghaei 7615aa0e51 fix: resolve seed encoding, search param, and wiki data source issues
- Fix seed-products.ts TS error (implicit any) and BOM handling
- Re-run full seed to restore correct Persian encoding in DB
- Fix productService query→search param mismatch
- Fix IngredientWiki hardcoded port 4000→use settingsStore
- Restore seed-products-data.json from git after accidental corruption
2026-07-11 15:40:49 +03:30

499 lines
25 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { useCartStore } from "../lib/store/cartStore";
import { usePetStore } from "../lib/store/usePetStore";
import { useUserStore } from "../lib/store/userStore";
import { toast } from "sonner";
import HeaderButton from "./HeaderButton";
import AuthModal from "./AuthModal";
import { cn } from "../lib/utils";
const MENU_ITEMS = [
{
id: "joints",
title: "مفاصل و استخوان",
icon: <Pill className="w-5 h-5" />,
solutions: ["آرتروز سگ‌های پیر", "رشد استخوانی توله‌سگ", "تقویت رباط و تاندون"]
},
{
id: "immune",
title: "تقویت سیستم ایمنی و گوارش",
icon: <ShieldCheck className="w-5 h-5" />,
solutions: ["پیشگیری از بیماری", "رفع اسهال و یبوست", "پروبیوتیک‌ها"]
},
{
id: "energy",
title: "ویتامین‌ها و انرژی‌بخش‌ها",
icon: <HeartPulse className="w-5 h-5" />,
solutions: ["مکمل‌های مولتی‌ویتامین", "افزایش اشتها", "رشد و بلوغ"]
},
{
id: "special-care",
title: "مراقبت‌های ویژه (پوست، دندان و چشم)",
icon: <Sparkles className="w-5 h-5" />,
solutions: ["سلامت پوست و مفاصل", "رفع جرم دندان", "شستشوی چشم"]
}
];
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 petMenuRef = useRef<HTMLDivElement>(null);
const { getTotalItems } = useCartStore();
const { pets, activePetId, setActivePet, getActivePet } = usePetStore();
const { role, isLoggedIn, logout, profile } = useUserStore();
const activePet = getActivePet();
// 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>ارسال رایگان برای سفارشهای بالای ۱۵۰ هزار تومان</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 */}
<div
className="flex-shrink-0 flex items-center gap-3 cursor-pointer group"
onClick={() => {
onNavigate("home");
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">
<span className="text-canina-blue font-black text-2xl tracking-tighter leading-none italic font-inter">Canina <span className="text-sm not-italic font-medium border-l-2 border-medical-gray-100 pl-2 ml-2 font-vazir">ایران</span></span>
<span className="text-[10px] text-medical-gray-400 font-bold uppercase tracking-widest leading-none mt-1.5 font-vazir">نماینده رسمی Canina Pharma GmbH آلمان</span>
</div>
</div>
</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`}>
راهکارهای درمانی
<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"
>
{MENU_ITEMS.map((item, idx) => (
<div
key={idx}
className="group/item cursor-pointer"
onClick={() => {
onShopNavigate(item.id);
setIsMegaMenuOpen(false);
}}
>
<div className="flex items-center gap-4 mb-4">
<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>
</div>
<ul className="space-y-2.5 border-r-2 border-medical-gray-50 pr-4">
{item.solutions.map((sol, sIdx) => (
<li
key={sIdx}
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"
onClick={(e) => {
e.stopPropagation();
onShopNavigate(item.id, sol);
setIsMegaMenuOpen(false);
}}
>
{sol}
</li>
))}
</ul>
</div>
))}
</motion.div>
)}
</AnimatePresence>
</div>
{[
{ id: 'shop', label: 'محصولات تخصصی', action: () => onShopNavigate() },
{ id: 'wiki', label: 'دانشنامه علمی', action: () => onNavigate('wiki') },
{ id: 'blog', label: 'مجله سلامت پت', action: () => onNavigate('blog') },
{ id: 'profile', label: 'شناسنامه پتها', action: () => onNavigate({ view: 'profile', subview: 'index' }) }
].map((item) => (
<button
key={item.id}
onClick={item.action}
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] ${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}
</button>
))}
</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: 350, opacity: 1 }}
exit={{ width: 0, opacity: 0 }}
transition={{ type: "spring", damping: 20, stiffness: 200 }}
className="absolute right-0 top-1/2 -translate-y-1/2 bg-white border-2 border-canina-blue rounded-2xl shadow-xl overflow-hidden h-12 flex items-center pr-10"
>
<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="جستجوی محصولات، مقالات سلامت و..."
className="w-full bg-transparent px-4 py-2 text-[12px] font-bold outline-none font-vazir text-medical-gray-900"
/>
<button type="submit" className="px-4 py-2 bg-canina-blue text-white text-[10px] font-black hover:bg-medical-gray-900 transition-colors">
جستجو
</button>
</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" : "")}
onClick={() => setIsSearchFocused(!isSearchFocused)}
>
<Search className="w-5 h-5" />
</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">تغییر پت یا مدیریت حساب</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" />
ثبت همدم (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" />
کیف پول و سفارشات
</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" />
خروج از حساب کانینا
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
) : (
<HeaderButton
variant="outline"
className="px-6 h-12 text-sm font-black"
onClick={() => setIsAuthModalOpen(true)}
icon={<LogIn className="w-5 h-5" />}
>
ورود / ثبتنام
</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">سبد خرید</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">
<div className="flex items-center gap-3">
<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-inter">Canina</span>
</div>
<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>راهکارهای درمانی</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"
>
{MENU_ITEMS.map((item, idx) => (
<div key={idx} className="py-1">
<div
className="text-[13px] font-bold text-medical-gray-800 font-vazir hover:text-canina-blue cursor-pointer flex items-center gap-2"
onClick={() => {
onShopNavigate(item.id);
setIsMobileMenuOpen(false);
}}
>
<span className="text-canina-blue/80">{item.icon}</span>
<span>{item.title}</span>
</div>
<ul className="mt-1 space-y-1 mr-6">
{item.solutions.map((sol, sIdx) => (
<li
key={sIdx}
className="text-[11px] text-medical-gray-500 hover:text-canina-blue cursor-pointer py-1 font-vazir"
onClick={(e) => {
e.stopPropagation();
onShopNavigate(item.id, sol);
setIsMobileMenuOpen(false);
}}
>
{sol}
</li>
))}
</ul>
</div>
))}
</motion.div>
)}
</AnimatePresence>
</div>
{[
{ id: 'shop', label: 'محصولات تخصصی', action: () => onShopNavigate() },
{ id: 'wiki', label: 'دانشنامه علمی', action: () => onNavigate('wiki') },
{ id: 'blog', label: 'مجله سلامت پت', action: () => onNavigate('blog') },
{ id: 'profile', label: 'شناسنامه پتها', action: () => onNavigate({ view: 'profile', subview: 'index' }) }
].map((item) => (
<button
key={item.id}
onClick={() => {
item.action();
setIsMobileMenuOpen(false);
}}
className={`w-full 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}
</button>
))}
</nav>
</motion.div>
</>
)}
</AnimatePresence>
</>
);
}