canina/frontend/application/components/MobileBottomNav.tsx
parsa aghaei 8e4ee9849c
Some checks failed
Deploy Canina / deploy (push) Successful in 3m37s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 7s
fix(catalog): fix mobile navigation, catalog preorder priority, admin settings and buybox
2026-09-13 07:44:55 +03:30

572 lines
24 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, useEffect, useCallback, useRef } from "react";
import Link from "next/link";
import Image from "next/image";
import { usePathname, useRouter } from "next/navigation";
import {
Home,
ShoppingBag,
ShoppingCart,
Menu,
Pill,
ShieldCheck,
HeartPulse,
BookOpen,
FileText,
Video,
Building2,
FileHeart,
User,
Search,
ChevronLeft,
Dog,
PhoneCall,
Sparkles,
ArrowRight
} from "lucide-react";
import { useCartStore } from "../lib/store/cartStore";
import { useUserStore } from "../lib/store/userStore";
import { useUIStore } from "../lib/store/uiStore";
import { useSettingsStore } from "../lib/store/settingsStore";
import { useCatalogMode } from "../lib/useCatalogMode";
import { productService } from "../lib/services/productService";
import { Product } from "../lib/data/products";
import { toPersian } from "../lib/utils";
const SOLUTION_ITEMS = [
{
id: "joints",
title: "مفاصل و استخوان",
icon: <Pill className="w-5 h-5 text-canina-blue" />,
solutions: ["لنگیدن", "سختی در بلند شدن", "درد مفاصل", "رشد سریع توله‌سگ", "پاهای پرانتزی", "ضعف تاندون"]
},
{
id: "immune",
title: "تقویت سیستم ایمنی و گوارش",
icon: <ShieldCheck className="w-5 h-5 text-emerald-600" />,
solutions: ["ضعف بعد از بیماری", "بی‌اشتهایی", "بعد از زایمان", "اسهال", "مشکلات گوارشی"]
},
{
id: "energy",
title: "ویتامین‌ها و انرژی‌بخش‌ها",
icon: <HeartPulse className="w-5 h-5 text-amber-500" />,
solutions: ["نفس‌نفس زدن", "خستگی", "سن بالا", "بی‌حالی"]
},
{
id: "special-care",
title: "مراقبت‌های ویژه (پوست، دندان و چشم)",
icon: <Sparkles className="w-5 h-5 text-indigo-500" />,
solutions: ["ریزش مو", "خشکی پوست", "خارش", "جرم دندان", "سوزش چشم"]
}
];
export default function MobileBottomNav({
onPrescriptionOpen,
}: {
onPrescriptionOpen?: () => void;
}) {
const pathname = usePathname();
const router = useRouter();
const [activeSheet, setActiveSheet] = useState<"shop" | "menu" | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState<Product[]>([]);
const [isSearching, setIsSearching] = useState(false);
const [isMounted, setIsMounted] = useState(false);
const isNavigatingBackRef = useRef(false);
const { getTotalItems } = useCartStore();
const setCartOpen = useUIStore((state) => state.setCartOpen);
const setB2BPortalOpen = useUIStore((state) => state.setB2BPortalOpen);
const isB2BEnabled = useSettingsStore((s) => s.getBoolean('b2bRegistrationOpen', true) && s.getBoolean('b2b_enabled', true));
const isPrescriptionEnabled = useSettingsStore((s) => s.getText('PRESCRIPTION_UPLOAD_ENABLED', 'true') !== 'false');
const { isCatalogOnly, allowCart } = useCatalogMode();
const isCartDisabled = isCatalogOnly && !allowCart;
const { isLoggedIn, setAuthModalOpen } = useUserStore();
useEffect(() => {
setIsMounted(true);
}, []);
// Close sheet with history navigation support (Back button dismiss)
const closeSheet = useCallback((shouldPopHistory: boolean | React.MouseEvent = true) => {
if (!activeSheet) return;
setActiveSheet(null);
const doPop = typeof shouldPopHistory === "boolean" ? shouldPopHistory : true;
if (doPop && typeof window !== "undefined" && window.location.hash === "#sheet") {
isNavigatingBackRef.current = true;
window.history.back();
} else if (!doPop && typeof window !== "undefined" && window.location.hash === "#sheet") {
// Remove hash without triggering history.back() which cancels router navigation
window.history.replaceState(null, "", window.location.pathname + window.location.search);
}
}, [activeSheet]);
const navigateAndClose = useCallback((href: string) => {
closeSheet(false);
router.push(href);
}, [closeSheet, router]);
const openSheet = useCallback((sheet: "shop" | "menu") => {
setActiveSheet(sheet);
if (typeof window !== "undefined") {
window.history.pushState({ mobileSheet: sheet }, "", "#sheet");
}
}, []);
// Handle hardware / browser back button popstate
useEffect(() => {
const handlePopState = () => {
if (isNavigatingBackRef.current) {
isNavigatingBackRef.current = false;
return;
}
if (activeSheet) {
setActiveSheet(null);
}
};
window.addEventListener("popstate", handlePopState);
return () => window.removeEventListener("popstate", handlePopState);
}, [activeSheet]);
// Close sheet on route change
useEffect(() => {
setActiveSheet(null);
setSearchQuery("");
setSearchResults([]);
}, [pathname]);
// Live search debounced query
useEffect(() => {
if (!searchQuery.trim()) {
setSearchResults([]);
setIsSearching(false);
return;
}
const timer = setTimeout(async () => {
setIsSearching(true);
try {
const res = await productService.getProducts({ query: searchQuery.trim(), limit: 6 });
setSearchResults(res.data || []);
} catch (err) {
console.error("Live search failed:", err);
} finally {
setIsSearching(false);
}
}, 250);
return () => clearTimeout(timer);
}, [searchQuery]);
const handleSearchSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (searchQuery.trim()) {
const q = searchQuery.trim();
closeSheet(false);
router.push(`/shop?search=${encodeURIComponent(q)}`);
} else {
closeSheet(false);
router.push("/shop");
}
};
const handleAccountClick = () => {
if (isLoggedIn) {
router.push("/dashboard");
} else {
setAuthModalOpen(true);
}
};
const totalCartItems = isMounted ? getTotalItems() : 0;
const isHomeActive = pathname === "/";
const isShopActive = pathname.startsWith("/shop") || activeSheet === "shop";
const isAccountActive = pathname.startsWith("/dashboard");
return (
<>
{/* 1. SHOP & SOLUTIONS BOTTOM SHEET (Modal for Shop Tab) */}
{activeSheet === "shop" && (
<div
className="lg:hidden fixed inset-0 z-50 flex items-end justify-center bg-black/50 backdrop-blur-xs font-vazir"
onClick={closeSheet}
>
<div
className="w-full max-w-lg bg-white rounded-t-3xl p-5 border-t border-medical-gray-200 shadow-2xl max-h-[85vh] flex flex-col animate-in slide-in-from-bottom duration-200"
onClick={(e) => e.stopPropagation()}
dir="rtl"
>
{/* Grab handle */}
<div className="w-10 h-1 bg-medical-gray-200 rounded-full mx-auto mb-3 shrink-0" />
{/* Header with Direct Shop Link */}
<div className="flex items-center justify-between pb-3 border-b border-medical-gray-100 mb-3 shrink-0">
<div className="flex items-center gap-2">
<div className="p-1.5 bg-canina-blue/10 rounded-xl text-canina-blue">
<ShoppingBag className="w-4 h-4" />
</div>
<div>
<h3 className="font-black text-sm text-medical-gray-900">فروشگاه و راهکارهای دارویی</h3>
<p className="text-[10px] text-medical-gray-400">جستجو در داروها، مکملها و علائم بالینی</p>
</div>
</div>
<button
type="button"
onClick={closeSheet}
className="text-xs text-medical-gray-400 hover:text-medical-gray-700 font-bold p-1"
>
بستن
</button>
</div>
{/* Search Input */}
<form onSubmit={handleSearchSubmit} className="relative mb-3 shrink-0">
<Search className="absolute right-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-medical-gray-400" />
<input
type="text"
placeholder="جستجوی دارو، ترکیب یا علامت بالینی..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2.5 pr-10 pl-14 text-xs outline-none focus:border-canina-blue font-vazir text-medical-gray-900"
autoFocus
/>
<button
type="submit"
className="absolute left-1.5 top-1/2 -translate-y-1/2 px-2.5 py-1 bg-canina-blue text-white rounded-lg text-[10px] font-bold"
>
جستجو
</button>
</form>
{/* Live Search Results (if typing) */}
{searchQuery.trim().length > 0 && (
<div className="mb-3 shrink-0 bg-medical-gray-50 rounded-2xl p-2.5 border border-medical-gray-200 max-h-56 overflow-y-auto">
<div className="flex items-center justify-between mb-2 px-1">
<span className="text-[10px] font-black text-medical-gray-500">
{isSearching ? "در حال جستجو..." : `نتایج زنده (${toPersian(searchResults.length)})`}
</span>
<button
type="button"
onClick={handleSearchSubmit}
className="text-[10px] font-bold text-canina-blue hover:underline flex items-center gap-0.5"
>
<span>مشاهده همه در فروشگاه</span>
<ArrowRight className="w-3 h-3 rotate-180" />
</button>
</div>
{searchResults.length === 0 && !isSearching ? (
<p className="text-center text-xs text-medical-gray-400 py-3">دارویی با این نام یافت نشد</p>
) : (
<div className="space-y-1.5">
{searchResults.map((prod) => (
<button
type="button"
key={prod.id}
onClick={() => navigateAndClose(`/products/${prod.slug || prod.id}`)}
className="w-full flex items-center gap-2.5 p-2 bg-white rounded-xl border border-medical-gray-100 hover:border-canina-blue transition-all text-right"
>
{prod.image ? (
<Image
src={prod.image}
alt={prod.name}
width={36}
height={36}
className="w-9 h-9 object-contain rounded-lg shrink-0"
/>
) : (
<div className="w-9 h-9 bg-medical-gray-100 rounded-lg flex items-center justify-center text-medical-gray-400 shrink-0">
<Pill className="w-4 h-4" />
</div>
)}
<div className="flex-1 min-w-0 text-right">
<div className="text-xs font-bold text-medical-gray-900 truncate">{prod.name}</div>
<div className="text-[10px] text-medical-gray-400 truncate">{prod.scientificTagline || prod.category}</div>
</div>
<div className="text-[11px] font-black text-canina-blue shrink-0">
{prod.price}
</div>
</button>
))}
</div>
)}
</div>
)}
{/* Direct Link to All Products */}
<button
type="button"
onClick={() => navigateAndClose("/shop")}
className="w-full flex items-center justify-between p-3 bg-canina-blue text-white rounded-2xl font-black text-xs shadow-md shadow-canina-blue/20 mb-3 shrink-0"
>
<div className="flex items-center gap-2">
<ShoppingBag className="w-4 h-4" />
<span>مشاهده کاتالوگ و همه محصولات</span>
</div>
<ChevronLeft className="w-4 h-4" />
</button>
{/* Prescription Upload Banner CTA if enabled */}
{isPrescriptionEnabled && (
<button
type="button"
onClick={() => {
closeSheet();
if (onPrescriptionOpen) {
onPrescriptionOpen();
}
}}
className="w-full flex items-center justify-between p-3 bg-gradient-to-r from-emerald-500 to-teal-600 text-white rounded-2xl font-black text-xs shadow-md shadow-emerald-500/15 mb-3 shrink-0"
>
<div className="flex items-center gap-2">
<div className="p-1.5 bg-white/20 rounded-xl">
<FileHeart className="w-4 h-4 text-white" />
</div>
<div className="text-right">
<div className="text-xs font-black">ثبت و استعلام نسخه دامپزشک</div>
<div className="text-[10px] text-emerald-100 font-medium">تامین مستقیم دارو و مکملهای اورجینال</div>
</div>
</div>
<ChevronLeft className="w-4 h-4" />
</button>
)}
{/* 4 Major Clinical Categories with Symptoms */}
<div className="overflow-y-auto flex-1 space-y-3 py-1 pr-0.5">
<div className="text-[11px] font-black text-medical-gray-400">دستهبندیها و راهکارهای درمانی:</div>
{SOLUTION_ITEMS.map((item) => (
<div key={item.id} className="p-3 bg-medical-gray-50/70 border border-medical-gray-100 rounded-2xl">
<button
type="button"
onClick={() => navigateAndClose(`/shop?category=${item.id}`)}
className="w-full flex items-center justify-between mb-2 text-right"
>
<div className="flex items-center gap-2">
{item.icon}
<span className="text-xs font-black text-medical-gray-800">{item.title}</span>
</div>
<span className="text-[10px] text-canina-blue font-bold flex items-center">
مشاهده داروها
<ChevronLeft className="w-3 h-3" />
</span>
</button>
<div className="flex flex-wrap gap-1.5">
{item.solutions.map((sol, sIdx) => (
<button
type="button"
key={sIdx}
onClick={() => navigateAndClose(`/shop?category=${item.id}&symptom=${encodeURIComponent(sol)}`)}
className="px-2.5 py-1 bg-white border border-medical-gray-200/80 rounded-lg text-[10px] font-bold text-medical-gray-600 hover:border-canina-blue hover:text-canina-blue transition-all"
>
{sol}
</button>
))}
</div>
</div>
))}
</div>
</div>
</div>
)}
{/* 2. MENU BOTTOM SHEET (Modal for Menu Tab) */}
{activeSheet === "menu" && (
<div
className="lg:hidden fixed inset-0 z-50 flex items-end justify-center bg-black/50 backdrop-blur-xs font-vazir"
onClick={closeSheet}
>
<div
className="w-full max-w-lg bg-white rounded-t-3xl p-5 border-t border-medical-gray-200 shadow-2xl max-h-[85vh] flex flex-col animate-in slide-in-from-bottom duration-200"
onClick={(e) => e.stopPropagation()}
dir="rtl"
>
{/* Grab handle */}
<div className="w-10 h-1 bg-medical-gray-200 rounded-full mx-auto mb-3 shrink-0" />
{/* Header */}
<div className="flex items-center justify-between pb-3 border-b border-medical-gray-100 mb-3 shrink-0">
<h3 className="font-black text-sm text-medical-gray-900">منوی بخشهای کنینا</h3>
<button
type="button"
onClick={closeSheet}
className="text-xs text-medical-gray-400 hover:text-medical-gray-700 font-bold p-1"
>
بستن
</button>
</div>
<div className="overflow-y-auto flex-1 space-y-3 py-1">
{/* Navigation Links Grid */}
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => navigateAndClose("/dashboard?tab=pets")}
className="flex items-center gap-2.5 p-3.5 bg-medical-gray-50 border border-medical-gray-200/80 rounded-2xl text-xs font-bold text-medical-gray-800 hover:border-canina-blue text-right"
>
<Dog className="w-4 h-4 text-canina-blue shrink-0" />
<span>شناسنامه پتها</span>
</button>
<button
type="button"
onClick={() => navigateAndClose("/catalog")}
className="flex items-center gap-2.5 p-3.5 bg-medical-gray-50 border border-medical-gray-200/80 rounded-2xl text-xs font-bold text-medical-gray-800 hover:border-canina-blue text-right"
>
<BookOpen className="w-4 h-4 text-emerald-600 shrink-0" />
<span>کاتالوگ دارویی</span>
</button>
<button
type="button"
onClick={() => navigateAndClose("/wiki")}
className="flex items-center gap-2.5 p-3.5 bg-medical-gray-50 border border-medical-gray-200/80 rounded-2xl text-xs font-bold text-medical-gray-800 hover:border-canina-blue text-right"
>
<FileText className="w-4 h-4 text-amber-500 shrink-0" />
<span>دانشنامه علمی</span>
</button>
<button
type="button"
onClick={() => navigateAndClose("/blog")}
className="flex items-center gap-2.5 p-3.5 bg-medical-gray-50 border border-medical-gray-200/80 rounded-2xl text-xs font-bold text-medical-gray-800 hover:border-canina-blue text-right"
>
<HeartPulse className="w-4 h-4 text-rose-500 shrink-0" />
<span>مجله سلامت پت</span>
</button>
<button
type="button"
onClick={() => navigateAndClose("/videos")}
className="flex items-center gap-2.5 p-3.5 bg-medical-gray-50 border border-medical-gray-200/80 rounded-2xl text-xs font-bold text-medical-gray-800 hover:border-canina-blue text-right"
>
<Video className="w-4 h-4 text-indigo-500 shrink-0" />
<span>ویدیوهای بالینی</span>
</button>
{isB2BEnabled && (
<button
type="button"
onClick={() => {
closeSheet();
setB2BPortalOpen(true);
}}
className="flex items-center gap-2.5 p-3.5 bg-medical-gray-50 border border-medical-gray-200/80 rounded-2xl text-xs font-bold text-medical-gray-800 hover:border-canina-blue text-right"
>
<Building2 className="w-4 h-4 text-teal-600 shrink-0" />
<span>خرید عمده (B2B)</span>
</button>
)}
</div>
{/* Contact & Support */}
<div className="pt-2 border-t border-medical-gray-100 space-y-1.5">
<button
type="button"
onClick={() => navigateAndClose("/contact")}
className="w-full flex items-center justify-between p-3 rounded-xl hover:bg-medical-gray-50 text-xs font-bold text-medical-gray-700 text-right"
>
<div className="flex items-center gap-2.5">
<PhoneCall className="w-4 h-4 text-canina-blue" />
<span>تماس با مرکز پشتیبانی و مشاوره</span>
</div>
<ChevronLeft className="w-4 h-4 text-medical-gray-300" />
</button>
</div>
</div>
</div>
</div>
)}
{/* 3. FIXED 5-TAB APP BOTTOM NAVIGATION BAR */}
<nav
aria-label="منوی ناوبری موبایل"
className="lg:hidden fixed bottom-0 inset-x-0 z-40 bg-white/95 backdrop-blur-md border-t border-medical-gray-200 shadow-[0_-4px_20px_rgba(0,0,0,0.06)] font-vazir"
dir="rtl"
>
<div className="max-w-md mx-auto px-2 h-16 flex items-center justify-between select-none">
{/* Tab 1: Home */}
<Link
href="/"
className={`flex flex-col items-center justify-center flex-1 py-1 transition-all ${
isHomeActive ? "text-canina-blue font-black" : "text-medical-gray-400 font-bold hover:text-medical-gray-700"
}`}
>
<Home className={`w-5 h-5 mb-1 ${isHomeActive ? "text-canina-blue" : "text-medical-gray-400"}`} />
<span className="text-[10px] leading-none">خانه</span>
</Link>
{/* Tab 2: Menu */}
<button
type="button"
onClick={() => {
if (activeSheet === "menu") closeSheet();
else openSheet("menu");
}}
className={`flex flex-col items-center justify-center flex-1 py-1 transition-all ${
activeSheet === "menu" ? "text-canina-blue font-black" : "text-medical-gray-400 font-bold hover:text-medical-gray-700"
}`}
>
<Menu className="w-5 h-5 mb-1" />
<span className="text-[10px] leading-none">منو</span>
</button>
{/* Tab 3: Shop (Opens Shop Sheet with Direct Shop Link + Solutions + Symptoms + Live Search) */}
<button
type="button"
onClick={() => {
if (activeSheet === "shop") closeSheet();
else openSheet("shop");
}}
className={`flex flex-col items-center justify-center flex-1 py-1 transition-all ${
isShopActive ? "text-canina-blue font-black" : "text-medical-gray-400 font-bold hover:text-medical-gray-700"
}`}
>
<div className="relative mb-1">
<ShoppingBag className={`w-5 h-5 ${isShopActive ? "text-canina-blue" : "text-medical-gray-400"}`} />
</div>
<span className="text-[10px] leading-none">فروشگاه</span>
</button>
{/* Tab 4: Cart */}
{!isCartDisabled && (
<button
type="button"
onClick={() => setCartOpen(true)}
className="flex flex-col items-center justify-center flex-1 py-1 text-medical-gray-400 font-bold hover:text-medical-gray-700 relative"
>
<div className="relative mb-1">
<ShoppingCart className="w-5 h-5" />
{totalCartItems > 0 && (
<span className="absolute -top-1.5 -right-2 min-w-[16px] h-4 px-1 bg-canina-gold text-canina-dark rounded-full text-[9px] font-black flex items-center justify-center leading-none shadow-xs">
{toPersian(totalCartItems)}
</span>
)}
</div>
<span className="text-[10px] leading-none">سبد خرید</span>
</button>
)}
{/* Tab 5: Account (Direct to Dashboard if logged in, otherwise opens Auth Modal) */}
<button
type="button"
onClick={handleAccountClick}
className={`flex flex-col items-center justify-center flex-1 py-1 transition-all ${
isAccountActive ? "text-canina-blue font-black" : "text-medical-gray-400 font-bold hover:text-medical-gray-700"
}`}
>
<User className={`w-5 h-5 mb-1 ${isAccountActive ? "text-canina-blue" : "text-medical-gray-400"}`} />
<span className="text-[10px] leading-none">حساب من</span>
</button>
</div>
</nav>
</>
);
}