canina/frontend/application/components/MobileBottomNav.tsx
parsa aghaei 1c98f1bfa3
Some checks failed
Deploy Canina / deploy (push) Successful in 2m31s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 0s
feat(mobile): app-like bottom navigation, mobile top logo ticker, and header optimization
2026-09-06 10:45:30 +03:30

438 lines
19 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 } from "react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import {
Home,
ShoppingBag,
Sparkles,
ShoppingCart,
Menu,
Pill,
ShieldCheck,
HeartPulse,
BookOpen,
FileText,
Video,
Building2,
FileHeart,
User,
LogOut,
Search,
ChevronLeft,
Dog,
PhoneCall
} 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 { toast } from "sonner";
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 [isSolutionsOpen, setIsSolutionsOpen] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [isMounted, setIsMounted] = useState(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 { isLoggedIn, profile, logout, setAuthModalOpen } = useUserStore();
useEffect(() => {
setIsMounted(true);
}, []);
useEffect(() => {
setIsSolutionsOpen(false);
setIsMenuOpen(false);
}, [pathname]);
const handleSearchSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (searchQuery.trim()) {
setIsSolutionsOpen(false);
router.push(`/shop?q=${encodeURIComponent(searchQuery.trim())}`);
}
};
const totalCartItems = isMounted ? getTotalItems() : 0;
const isHomeActive = pathname === "/";
const isShopActive = pathname === "/shop";
return (
<>
{/* 1. SOLUTIONS BOTTOM SHEET */}
{isSolutionsOpen && (
<div
className="lg:hidden fixed inset-0 z-50 flex items-end justify-center bg-black/50 backdrop-blur-xs font-vazir"
onClick={() => setIsSolutionsOpen(false)}
>
<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"
>
<div className="w-10 h-1 bg-medical-gray-200 rounded-full mx-auto mb-3 shrink-0" />
<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">
<Sparkles 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={() => setIsSolutionsOpen(false)}
className="text-xs text-medical-gray-400 hover:text-medical-gray-700 font-bold p-1"
>
بستن
</button>
</div>
<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-3 text-xs outline-none focus:border-canina-blue font-vazir"
/>
</form>
{isPrescriptionEnabled && (
<button
type="button"
onClick={() => {
setIsSolutionsOpen(false);
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>
)}
<div className="overflow-y-auto flex-1 space-y-3.5 py-1 pr-0.5">
{SOLUTION_ITEMS.map((item) => (
<div key={item.id} className="p-3 bg-medical-gray-50/70 border border-medical-gray-100 rounded-2xl">
<Link
href={`/shop?category=${item.id}`}
onClick={() => setIsSolutionsOpen(false)}
className="flex items-center justify-between mb-2.5"
>
<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>
</Link>
<div className="flex flex-wrap gap-1.5">
{item.solutions.map((sol, sIdx) => (
<Link
key={sIdx}
href={`/shop?category=${item.id}&symptom=${encodeURIComponent(sol)}`}
onClick={() => setIsSolutionsOpen(false)}
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}
</Link>
))}
</div>
</div>
))}
</div>
</div>
</div>
)}
{/* 2. MENU & USER PROFILE BOTTOM SHEET */}
{isMenuOpen && (
<div
className="lg:hidden fixed inset-0 z-50 flex items-end justify-center bg-black/50 backdrop-blur-xs font-vazir"
onClick={() => setIsMenuOpen(false)}
>
<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"
>
<div className="w-10 h-1 bg-medical-gray-200 rounded-full mx-auto mb-3 shrink-0" />
<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={() => setIsMenuOpen(false)}
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">
{isLoggedIn ? (
<div className="p-3.5 bg-medical-gray-50 rounded-2xl border border-medical-gray-100 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-canina-blue/10 flex items-center justify-center text-canina-blue font-black">
<User className="w-5 h-5" />
</div>
<div>
<div className="text-xs font-black text-medical-gray-900">
{profile.firstName ? `${profile.firstName} ${profile.lastName || ''}` : 'کاربر گرامی'}
</div>
<div className="text-[10px] text-medical-gray-400 font-mono mt-0.5">
{profile.mobile || profile.email}
</div>
</div>
</div>
<Link
href="/dashboard"
onClick={() => setIsMenuOpen(false)}
className="px-3 py-1.5 bg-canina-blue text-white rounded-xl text-xs font-bold"
>
داشبورد
</Link>
</div>
) : (
<button
type="button"
onClick={() => {
setIsMenuOpen(false);
setAuthModalOpen(true);
}}
className="w-full flex items-center justify-between p-3.5 bg-medical-gray-900 text-white rounded-2xl font-black text-xs shadow-md"
>
<div className="flex items-center gap-2.5">
<User className="w-5 h-5 text-canina-gold" />
<span>ورود یا ثبتنام در کنینا ایران</span>
</div>
<ChevronLeft className="w-4 h-4" />
</button>
)}
<div className="grid grid-cols-2 gap-2 pt-2">
<Link
href="/dashboard?tab=pets"
onClick={() => setIsMenuOpen(false)}
className="flex items-center gap-2.5 p-3 bg-white border border-medical-gray-200/80 rounded-2xl text-xs font-bold text-medical-gray-800 hover:border-canina-blue"
>
<Dog className="w-4 h-4 text-canina-blue" />
<span>شناسنامه پتها</span>
</Link>
<Link
href="/catalog"
onClick={() => setIsMenuOpen(false)}
className="flex items-center gap-2.5 p-3 bg-white border border-medical-gray-200/80 rounded-2xl text-xs font-bold text-medical-gray-800 hover:border-canina-blue"
>
<BookOpen className="w-4 h-4 text-emerald-600" />
<span>کاتالوگ دارویی</span>
</Link>
<Link
href="/wiki"
onClick={() => setIsMenuOpen(false)}
className="flex items-center gap-2.5 p-3 bg-white border border-medical-gray-200/80 rounded-2xl text-xs font-bold text-medical-gray-800 hover:border-canina-blue"
>
<FileText className="w-4 h-4 text-amber-500" />
<span>دانشنامه علمی</span>
</Link>
<Link
href="/blog"
onClick={() => setIsMenuOpen(false)}
className="flex items-center gap-2.5 p-3 bg-white border border-medical-gray-200/80 rounded-2xl text-xs font-bold text-medical-gray-800 hover:border-canina-blue"
>
<HeartPulse className="w-4 h-4 text-rose-500" />
<span>مجله سلامت پت</span>
</Link>
<Link
href="/videos"
onClick={() => setIsMenuOpen(false)}
className="flex items-center gap-2.5 p-3 bg-white border border-medical-gray-200/80 rounded-2xl text-xs font-bold text-medical-gray-800 hover:border-canina-blue"
>
<Video className="w-4 h-4 text-indigo-500" />
<span>ویدیوهای بالینی</span>
</Link>
{isB2BEnabled && (
<button
type="button"
onClick={() => {
setIsMenuOpen(false);
setB2BPortalOpen(true);
}}
className="flex items-center gap-2.5 p-3 bg-white 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" />
<span>خرید عمده (B2B)</span>
</button>
)}
</div>
<div className="pt-2 border-t border-medical-gray-100 space-y-1.5">
<Link
href="/contact"
onClick={() => setIsMenuOpen(false)}
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"
>
<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" />
</Link>
{isLoggedIn && (
<button
type="button"
onClick={() => {
logout();
setIsMenuOpen(false);
toast.success('از حساب کاربری خارج شدید');
}}
className="w-full flex items-center gap-2.5 p-3 text-rose-600 rounded-xl font-bold text-xs hover:bg-rose-50 transition-colors text-right"
>
<LogOut className="w-4 h-4" />
<span>خروج از حساب</span>
</button>
)}
</div>
</div>
</div>
</div>
)}
{/* 3. FIXED FLOATING BOTTOM APP 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/80 shadow-[0_-4px_20px_rgba(0,0,0,0.06)] font-vazir"
dir="rtl"
>
<div className="max-w-md mx-auto px-3 h-16 flex items-center justify-between select-none">
<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>
<Link
href="/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"
}`}
>
<ShoppingBag className={`w-5 h-5 mb-1 ${isShopActive ? "text-canina-blue" : "text-medical-gray-400"}`} />
<span className="text-[10px] leading-none">فروشگاه</span>
</Link>
<div className="flex-1 flex justify-center -mt-5">
<button
type="button"
onClick={() => setIsSolutionsOpen(!isSolutionsOpen)}
className="w-12 h-12 rounded-full bg-gradient-to-tr from-canina-blue to-teal-700 text-white shadow-lg shadow-canina-blue/30 flex items-center justify-center active:scale-95 transition-transform border-4 border-white"
title="راهکارها و علائم"
>
<Sparkles className="w-5 h-5 text-canina-gold animate-pulse" />
</button>
</div>
<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>
<button
type="button"
onClick={() => setIsMenuOpen(!isMenuOpen)}
className={`flex flex-col items-center justify-center flex-1 py-1 transition-all ${
isMenuOpen ? "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>
</div>
</nav>
</>
);
}