fix(mobile): resolve cart back navigation, tooltip positioning, product dosage calculator and badges
Some checks failed
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 3m46s
Deploy Canina / deploy (push) Has been cancelled

This commit is contained in:
parsa aghaei 2026-08-26 11:01:48 +03:30
parent 753b3dd7ec
commit 9371fd79e1
15 changed files with 250172 additions and 248138 deletions

View File

@ -55,17 +55,30 @@ export default function CartDrawer({ isOpen, onClose, onCheckout, onShopNavigate
}
}, [hasLachsOl, hasCanhydrox]);
// Lock background body scroll when cart drawer is open
// Lock background body scroll and handle browser back button to close drawer
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
if (typeof window !== "undefined") {
window.history.pushState({ modal: "cart_drawer" }, "");
}
} else {
document.body.style.overflow = "unset";
}
const handlePopState = () => {
if (isOpen) {
onClose();
}
};
window.addEventListener("popstate", handlePopState);
return () => {
document.body.style.overflow = "unset";
window.removeEventListener("popstate", handlePopState);
};
}, [isOpen]);
}, [isOpen, onClose]);
return (
<AnimatePresence>

View File

@ -18,6 +18,7 @@ import {
Bell,
Sparkles,
ChevronDown,
ChevronUp,
Activity,
Heart,
Zap,
@ -25,6 +26,9 @@ import {
ShoppingBag,
Thermometer,
AlertTriangle,
AlertCircle,
Dog,
Cat,
Share2,
Phone,
Download,
@ -84,6 +88,7 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
const [showRefillModal, setShowRefillModal] = useState(false);
const [isMounted, setIsMounted] = useState(false);
const [dosageConfig, setDosageConfig] = useState<DosageConfig | null>(null);
const [isFullDescriptionOpen, setIsFullDescriptionOpen] = useState(false);
const [activeImage, setActiveImage] = useState<string | null>(null);
const [isImageZoomOpen, setIsImageZoomOpen] = useState(false);
@ -156,6 +161,22 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
}
}, [activePet, setPetType, setWeight]);
// Pet compatibility calculation matching ArchivePage logic
const compatibility = useMemo(() => {
if (!activePet || !fullProduct) return null;
const sameSpecies = fullProduct.suitableFor === activePet.type || fullProduct.suitableFor === "هر دو";
const matchedSymptom = (activePet.medicalConditions || []).find(mc =>
(fullProduct.symptoms || []).some(s => mc.includes(s) || s.includes(mc))
);
if (!sameSpecies) return { type: 'alert', text: `مخصوص ${fullProduct.suitableFor}` };
if (matchedSymptom) return { type: 'success', text: `توصیه شده برای ${matchedSymptom} ${activePet.name}` };
if (sameSpecies) return { type: 'neutral', text: `مناسب برای ${activePet.name}` };
return null;
}, [fullProduct, activePet]);
const calculation = useMemo(() => {
if (!fullProduct) return null;
@ -166,7 +187,8 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
if (dosageConfig) {
const baseW = dosageConfig.baseWeightKg || 10;
const baseD = dosageConfig.baseDose || 1;
const calculated = Math.round((weight / baseW) * baseD);
const ageMultiplier = petType === "young" ? (dosageConfig.youngMultiplier || 1.25) : 1;
const calculated = Math.round((weight / baseW) * baseD * ageMultiplier);
dailyDose = Math.max(1, calculated);
if (dosageConfig.maxDailyDose && dosageConfig.maxDailyDose > 0) {
dailyDose = Math.min(dailyDose, dosageConfig.maxDailyDose);
@ -292,6 +314,20 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
گرید دارویی اختصاصی
</div>
</div>
{/* Species Suitable Badges (Dog / Cat) */}
<div className="absolute bottom-4 left-4 flex gap-1.5 z-10">
{(product.suitableFor === "سگ" || product.suitableFor === "هر دو") && (
<div className="w-7 h-7 bg-white/90 backdrop-blur-sm rounded-lg flex items-center justify-center text-medical-gray-600 border border-medical-gray-200 shadow-sm" title="مناسب برای سگ">
<Dog className="w-4 h-4 text-canina-blue" />
</div>
)}
{(product.suitableFor === "گربه" || product.suitableFor === "هر دو") && (
<div className="w-7 h-7 bg-white/90 backdrop-blur-sm rounded-lg flex items-center justify-center text-medical-gray-600 border border-medical-gray-200 shadow-sm" title="مناسب برای گربه">
<Cat className="w-4 h-4 text-canina-blue" />
</div>
)}
</div>
<SafeImage
src={activeImage || product.image}
alt={product.name}
@ -359,6 +395,16 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
{product.specialBadge}
</div>
)}
{compatibility && (
<div className={`px-3 py-1 text-[10px] font-black uppercase tracking-widest rounded-full shadow-sm flex items-center gap-1 whitespace-nowrap ${
compatibility.type === 'alert' ? 'bg-amber-100 text-amber-800 border border-amber-200' :
compatibility.type === 'success' ? 'bg-emerald-100 text-emerald-800 border border-emerald-200' :
'bg-medical-gray-100 text-medical-gray-700 border border-medical-gray-200'
}`}>
{compatibility.type === 'alert' ? <AlertCircle className="w-3 h-3 shrink-0" /> : compatibility.type === 'success' ? <Heart className="w-3 h-3 shrink-0 text-emerald-600" /> : <Sparkles className="w-3 h-3 shrink-0 text-canina-blue" />}
<span>{compatibility.text}</span>
</div>
)}
</div>
<div className="flex flex-col gap-1.5 mb-4 text-center md:text-right w-full">
<h1 dir="rtl" className="text-xl sm:text-2xl md:text-3xl lg:text-4xl font-black text-medical-gray-900 leading-tight font-vazir">
@ -441,17 +487,44 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
{/* Comprehensive Product Description Section (Below the fold) */}
{fullProduct.description && (
<section className="bg-white border border-medical-gray-200 rounded-[2.5rem] p-8 md:p-10 shadow-md space-y-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-canina-blue text-white rounded-2xl flex items-center justify-center shadow-lg">
<Info className="w-6 h-6" />
<section className="bg-white border border-medical-gray-200 rounded-[2rem] sm:rounded-[2.5rem] p-5 sm:p-8 md:p-10 shadow-md space-y-4 sm:space-y-6">
<div className="flex items-center gap-3 sm:gap-4">
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-canina-blue text-white rounded-2xl flex items-center justify-center shadow-lg shrink-0">
<Info className="w-5 h-5 sm:w-6 sm:h-6" />
</div>
<h3 className="text-2xl font-black text-medical-gray-900 italic font-vazir">توضیحات و معرفی محصول</h3>
<h3 className="text-xl sm:text-2xl font-black text-medical-gray-900 italic font-vazir">توضیحات و معرفی محصول</h3>
</div>
<div className="text-base text-medical-gray-600 font-medium leading-loose font-vazir space-y-4 pt-4 border-t border-medical-gray-100">
{fullProduct.description.split('\n').filter(Boolean).map((para, pIdx) => (
<p key={pIdx}>{para.trim()}</p>
))}
<div className="text-sm sm:text-base text-medical-gray-600 font-medium leading-relaxed sm:leading-loose font-vazir space-y-3 pt-3 border-t border-medical-gray-100 relative">
{(() => {
const paragraphs = fullProduct.description.split('\n').filter(Boolean);
const shouldTruncate = paragraphs.length > 2 || fullProduct.description.length > 300;
const displayedParagraphs = (!isFullDescriptionOpen && shouldTruncate)
? paragraphs.slice(0, 2)
: paragraphs;
return (
<>
<div className="space-y-2">
{displayedParagraphs.map((para, pIdx) => (
<p key={pIdx}>{para.trim()}</p>
))}
</div>
{shouldTruncate && (
<div className="pt-2 flex justify-center">
<button
type="button"
onClick={() => setIsFullDescriptionOpen(prev => !prev)}
className="inline-flex items-center gap-1.5 px-4 py-2 bg-medical-gray-50 hover:bg-canina-blue/10 text-canina-blue rounded-xl text-xs font-black transition-all border border-medical-gray-200 hover:border-canina-blue/30 cursor-pointer"
>
<span>{isFullDescriptionOpen ? 'بستن توضیحات' : 'مشاهده توضیحات کامل و بیشتر'}</span>
{isFullDescriptionOpen ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
</div>
)}
</>
);
})()}
</div>
</section>
)}
@ -465,16 +538,16 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
fullProduct.nameFa?.includes('کرم') ||
fullProduct.nameFa?.includes('قطره تمیزکننده') ||
fullProduct.nameFa?.includes('اسپری')) && (
<section className="bg-white rounded-[2.5rem] border border-medical-gray-200 p-8 md:p-10 shadow-md space-y-8">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-canina-blue text-white rounded-2xl flex items-center justify-center shadow-lg">
<Clock className="w-6 h-6" />
<section className="bg-white rounded-[2rem] sm:rounded-[2.5rem] border border-medical-gray-200 p-5 sm:p-8 md:p-10 shadow-md space-y-6 sm:space-y-8">
<div className="flex items-center gap-3 sm:gap-4">
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-canina-blue text-white rounded-2xl flex items-center justify-center shadow-lg shrink-0">
<Clock className="w-5 h-5 sm:w-6 sm:h-6" />
</div>
<div>
<h3 className="text-lg sm:text-2xl font-black text-medical-gray-900 italic font-vazir whitespace-nowrap">
<h3 className="text-base sm:text-2xl font-black text-medical-gray-900 italic font-vazir whitespace-nowrap">
{activePet ? `ماشین حساب هوشمند مصرف برای ${activePet.name}` : 'ماشین حساب هوشمند مصرف'}
</h3>
<p className="text-[11px] sm:text-xs text-medical-gray-400 font-bold font-vazir mt-0.5 sm:mt-1">
<p className="text-[10px] sm:text-xs text-medical-gray-400 font-bold font-vazir mt-0.5 sm:mt-1">
{activePet
? `تخمین دقیق دوز روزانه و ماندگاری هر بسته بر اساس وزن و سن ${activePet.name}`
: 'تخمین دقیق دوز روزانه و ماندگاری هر بسته بر اساس مشخصات پت شما'}
@ -482,14 +555,14 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
</div>
</div>
<div className="grid md:grid-cols-2 gap-8 items-center">
<div className="grid md:grid-cols-2 gap-6 sm:gap-8 items-center">
{/* Manual Slider Inputs */}
<div className="bg-medical-gray-50 rounded-[2rem] p-6 border border-medical-gray-100 space-y-6">
<div className="bg-medical-gray-50 rounded-2xl sm:rounded-[2rem] p-4 sm:p-6 border border-medical-gray-100 space-y-5">
<div className="flex items-center justify-between">
<span className="text-xs font-black text-medical-gray-500 font-vazir">
{activePet ? `وزن ${activePet.name} (کیلوگرم)` : 'وزن پت (کیلوگرم)'}
</span>
<span className="text-lg font-black text-canina-blue font-vazir">{toPersian(weight)} کگ</span>
<span className="text-base sm:text-lg font-black text-canina-blue font-vazir">{toPersian(weight)} کگ</span>
</div>
<input
type="range"
@ -504,45 +577,50 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
{activePet ? `سن ${activePet.name}` : 'سن حیوان'}
</span>
<div className="flex gap-2">
<button onClick={() => setPetType("young")} className={`flex-1 py-3 rounded-xl text-xs font-black transition-all border ${petType === "young" ? 'bg-canina-blue border-canina-blue text-white shadow-lg' : 'bg-white border-medical-gray-200 text-medical-gray-400'}`}>جوان</button>
<button onClick={() => setPetType("adult")} className={`flex-1 py-3 rounded-xl text-xs font-black transition-all border ${petType === "adult" ? 'bg-canina-blue border-canina-blue text-white shadow-lg' : 'bg-white border-medical-gray-200 text-medical-gray-400'}`}>بالغ</button>
<button onClick={() => setPetType("young")} className={`flex-1 py-2.5 sm:py-3 rounded-xl text-xs font-black transition-all border ${petType === "young" ? 'bg-canina-blue border-canina-blue text-white shadow-md' : 'bg-white border-medical-gray-200 text-medical-gray-400'}`}>جوان</button>
<button onClick={() => setPetType("adult")} className={`flex-1 py-2.5 sm:py-3 rounded-xl text-xs font-black transition-all border ${petType === "adult" ? 'bg-canina-blue border-canina-blue text-white shadow-md' : 'bg-white border-medical-gray-200 text-medical-gray-400'}`}>بالغ</button>
</div>
</div>
</div>
{/* Calculation Results Output */}
<div className="bg-canina-blue rounded-[2rem] p-8 text-white border-b-8 border-white/20 shadow-2xl flex flex-col justify-between h-full min-h-[240px]">
<div className="flex items-center justify-between mb-4 opacity-90 border-b border-white/10 pb-3">
<div className="flex items-center gap-2">
<Sparkles className="w-4 h-4 text-amber-300" />
<span className="text-[10px] font-bold uppercase tracking-widest font-vazir text-white">
{activePet ? `محاسبه‌گر دوره درمانی برای ${activePet.name}` : 'محاسبه‌گر دوره درمانی (AI Calculator)'}
<div className="bg-canina-blue rounded-2xl sm:rounded-[2rem] p-5 sm:p-8 text-white border-b-8 border-white/20 shadow-2xl flex flex-col justify-between h-full min-h-[220px]">
<div className="flex items-center justify-between mb-4 opacity-90 border-b border-white/10 pb-3 gap-2">
<div className="flex flex-col gap-0.5">
<div className="flex items-center gap-1.5">
<Sparkles className="w-3.5 h-3.5 text-amber-300 shrink-0" />
<span className="text-[11px] font-black font-vazir text-white whitespace-nowrap">
{activePet ? `محاسبه‌گر دوره درمانی ${activePet.name}` : 'محاسبه‌گر دوره درمانی'}
</span>
</div>
<span className="text-[9px] font-bold text-white/70 font-sans tracking-wide">
AI Calculator
</span>
</div>
<span className="text-[10px] bg-white/20 px-2.5 py-0.5 rounded-full font-black">
<span className="text-[10px] bg-white/20 px-2.5 py-1 rounded-full font-black whitespace-nowrap shrink-0">
پیشنهاد: {toPersian(itemQuantity)} بسته
</span>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<div className="text-3xl font-black font-vazir text-white">{toPersian(calculation.duration)} روز</div>
<div className="text-2xl sm:text-3xl font-black font-vazir text-white">{toPersian(calculation.duration)} روز</div>
<div className="text-[10px] font-bold text-white/80 font-vazir whitespace-nowrap mt-1">مدت تامین یک بسته</div>
</div>
<div className="text-left">
<div className="text-2xl font-black font-vazir text-white">{toPersian(calculation.dailyDose)}</div>
<div className="text-xl sm:text-2xl font-black font-vazir text-white">{toPersian(calculation.dailyDose)}</div>
<div className="text-[10px] font-bold text-white/80 font-vazir whitespace-nowrap mt-1">دوز روزانه ({calculation.unit === 'g' ? 'گرم' : calculation.unit})</div>
</div>
</div>
<div className="mt-4 pt-4 border-t border-white/10 flex items-center justify-between">
<span className="text-[11px] font-bold text-white/80">تخمین هزینه دوره ۳۰ روزه:</span>
<span className="text-sm font-black text-amber-300">
<div className="mt-4 pt-4 border-t border-white/10 flex items-center justify-between gap-2">
<span className="text-[11px] font-bold text-white/80 whitespace-nowrap">تخمین هزینه دوره ۳۰ روزه:</span>
<span className="text-xs sm:text-sm font-black text-amber-300 whitespace-nowrap">
{toPersian((Math.ceil(30 / (calculation.duration || 30)) * (product.priceValue || 0)).toLocaleString())} تومان
</span>
</div>
<p className="text-[10px] text-white/60 font-bold font-vazir mt-3 leading-relaxed">
<p className="text-[10px] text-white/70 font-bold font-vazir mt-3 leading-relaxed">
{activePet
? `* دوز دقیق بر اساس وزن ${activePet.name} (${toPersian(weight)} ک‌گ) محاسبه شده است؛ برای ماندگاری کامل دوره، خرید تعداد بسته‌های پیشنهادی توصیه می‌شود.`
: '* دوز دقیق بر اساس وزن پت محاسبه شده است؛ برای ماندگاری کامل دوره، خرید تعداد بسته‌های پیشنهادی توصیه می‌شود.'}
@ -553,20 +631,20 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
)}
{/* Ingredients Section */}
<section className="bg-white rounded-[2.5rem] border border-medical-gray-200 p-8 md:p-10 shadow-md space-y-8">
<div className="flex items-center gap-4 border-b border-medical-gray-100 pb-6">
<div className="w-12 h-12 bg-canina-blue text-white rounded-2xl flex items-center justify-center shadow-lg">
<FlaskConical className="w-6 h-6" />
<section className="bg-white rounded-[2rem] sm:rounded-[2.5rem] border border-medical-gray-200 p-5 sm:p-8 md:p-10 shadow-md space-y-6 sm:space-y-8">
<div className="flex items-center gap-3 sm:gap-4 border-b border-medical-gray-100 pb-4 sm:pb-6">
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-canina-blue text-white rounded-2xl flex items-center justify-center shadow-lg shrink-0">
<FlaskConical className="w-5 h-5 sm:w-6 sm:h-6" />
</div>
<h3 className="text-2xl md:text-3xl font-black text-medical-gray-900 italic font-vazir">ترکیبات و آنالیز علمی</h3>
<h3 className="text-xl sm:text-2xl md:text-3xl font-black text-medical-gray-900 italic font-vazir">ترکیبات و آنالیز علمی</h3>
</div>
<div className="space-y-8">
<div className="space-y-6 sm:space-y-8">
{/* Ingredients Cards */}
{product.main_ingredients && product.main_ingredients.length > 0 && (
<div>
<h4 className="text-xs font-black text-medical-gray-400 uppercase tracking-widest mb-4 font-vazir">مواد تشکیلدهنده برتر</h4>
<div className="flex flex-wrap gap-3">
<h4 className="text-xs font-black text-medical-gray-400 uppercase tracking-widest mb-3 font-vazir">مواد تشکیلدهنده برتر</h4>
<div className="flex flex-wrap gap-2.5 sm:gap-3">
{product.main_ingredients.map((ing, idx) => {
const termKey = termKeys.find(key => {
const termObj = scientificTerms[key] || SCIENTIFIC_TERMS[key];
@ -580,8 +658,8 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
termNameClean.includes(ingClean);
});
return (
<div key={idx} className="bg-medical-gray-50 border border-medical-gray-100 px-5 py-3 rounded-2xl flex items-center gap-3 group hover:border-canina-blue/30 transition-all">
<span className="text-sm font-bold text-medical-gray-700 font-vazir">
<div key={idx} className="bg-medical-gray-50 border border-medical-gray-100 px-3.5 py-2 sm:px-5 sm:py-3 rounded-xl sm:rounded-2xl flex items-center gap-2 sm:gap-3 group hover:border-canina-blue/30 transition-all">
<span className="text-xs sm:text-sm font-bold text-medical-gray-700 font-vazir">
{termKey ? (
<Tooltip termKey={termKey} onWikiNavigate={(id) => router.push(`/wiki?term=${id}`)}>
{ing}

View File

@ -1,5 +1,5 @@
"use client";
import React, { useState } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { Info, ExternalLink } from 'lucide-react';
import { SCIENTIFIC_TERMS } from '../lib/data/scientificTerms';
@ -13,30 +13,90 @@ interface TooltipProps {
export default function Tooltip({ termKey, children, onWikiNavigate }: TooltipProps) {
const [isVisible, setIsVisible] = useState(false);
const [positionClass, setPositionClass] = useState("left-1/2 -translate-x-1/2");
const [arrowClass, setArrowClass] = useState("left-1/2 -translate-x-1/2");
const containerRef = useRef<HTMLSpanElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
const scientificTerms = useSettingsStore(state => state.scientificTerms);
const termData = scientificTerms[termKey] || SCIENTIFIC_TERMS[termKey];
// Adjust horizontal position dynamically to prevent overflowing viewport edges
useEffect(() => {
if (isVisible && containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
const screenWidth = window.innerWidth;
if (rect.left < 140) {
// Too close to left edge (in LTR) or screen boundary
setPositionClass("left-0 translate-x-0");
setArrowClass("left-6 translate-x-0");
} else if (screenWidth - rect.right < 140) {
// Too close to right edge
setPositionClass("right-0 translate-x-0");
setArrowClass("right-6 translate-x-0");
} else {
setPositionClass("left-1/2 -translate-x-1/2");
setArrowClass("left-1/2 -translate-x-1/2");
}
}
}, [isVisible]);
// Click outside listener to dismiss on mobile
useEffect(() => {
if (!isVisible) return;
const handleClickOutside = (e: MouseEvent | TouchEvent) => {
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
setIsVisible(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("touchstart", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("touchstart", handleClickOutside);
};
}, [isVisible]);
if (!termData) return <>{children}</>;
const handleToggle = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setIsVisible(prev => !prev);
};
return (
<span
className="relative inline-block group cursor-help border-b border-dotted border-canina-blue/40"
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
ref={containerRef}
className="relative inline-block group cursor-pointer border-b border-dotted border-canina-blue/60 select-none"
onClick={handleToggle}
onMouseEnter={() => {
if (typeof window !== "undefined" && window.matchMedia?.("(hover: hover)")?.matches !== false) {
setIsVisible(true);
}
}}
onMouseLeave={() => {
if (typeof window !== "undefined" && window.matchMedia?.("(hover: hover)")?.matches !== false) {
setIsVisible(false);
}
}}
>
{children}
<AnimatePresence>
{isVisible && (
<motion.div
ref={tooltipRef}
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 bottom-full left-1/2 -translate-x-1/2 mb-3 w-72 p-5 bg-white text-medical-gray-900 rounded-2xl shadow-2xl border border-medical-gray-200 z-[100] text-sm"
onClick={(e) => e.stopPropagation()}
className={`absolute bottom-full mb-3 w-72 max-w-[calc(100vw-2rem)] p-4 sm:p-5 bg-white text-medical-gray-900 rounded-2xl shadow-2xl border border-medical-gray-200 z-[100] text-sm ${positionClass}`}
dir="rtl"
>
<div className="flex items-center gap-2 mb-2 text-canina-blue">
<Info className="w-4 h-4" />
<Info className="w-4 h-4 shrink-0" />
<span className="font-black text-[11px] uppercase tracking-widest text-canina-blue">دانشنامه علمی کنینا</span>
</div>
<h5 className="font-black text-base text-medical-gray-900 mb-2 leading-snug">{termData.term}</h5>
@ -52,11 +112,11 @@ export default function Tooltip({ termKey, children, onWikiNavigate }: TooltipPr
className="flex items-center justify-between text-canina-blue hover:text-blue-700 transition-colors text-xs font-black cursor-pointer pt-2 border-t border-medical-gray-100 w-full"
>
<span>مطالعه مقاله کامل</span>
<ExternalLink className="w-3.5 h-3.5" />
<ExternalLink className="w-3.5 h-3.5 shrink-0" />
</button>
)}
{/* Arrow */}
<div className="absolute top-full left-1/2 -translate-x-1/2 border-8 border-transparent border-t-white" />
<div className={`absolute top-full border-8 border-transparent border-t-white ${arrowClass}`} />
</motion.div>
)}
</AnimatePresence>

View File

@ -97,6 +97,7 @@ export interface DosageConfig {
baseDose: number;
maxDailyDose: number;
instructions: string;
youngMultiplier?: number;
}
export interface HomeApiResponse {

View File

@ -1,38 +1,26 @@
{
"0": "Roles",
"1": "app.module.ts",
"2": "SmsService",
"2": "SettingsController",
"3": "ProductService",
"4": "utils.ts",
"4": "SafeImage.tsx",
"5": "CmsController",
"6": "tickets.controller.ts",
"7": "Button.tsx",
"8": "ReportsController",
"8": "admin.module.ts",
"9": "devDependencies",
<<<<<<< HEAD
"10": "ReviewsController",
"10": "CreateReviewDto",
"11": "MediaSelector.tsx",
"12": "index.ts",
"13": "app-audit-verification.e2e-spec.js",
"14": "useSettingsStore",
=======
"10": "ReviewsService",
"11": "MediaSelector.tsx",
"12": "PetProfile.tsx",
"13": "app-audit-verification.e2e-spec.js",
"14": "UserDashboard.tsx",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"15": "src/services/api.ts",
"16": "DoctorQueryDto",
"17": "UserDashboard.tsx",
"17": "schema.ts",
"18": "JwtAuthGuard",
"19": "ProductsService",
"19": "RevalidationService",
"20": "CreateVideoDto",
<<<<<<< HEAD
"21": "Button.tsx",
=======
"21": "ProductPage.tsx",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"22": "lib/services/api.ts",
"23": "MenuService",
"24": "BE-001",
@ -44,9 +32,9 @@
"30": "DEVOPS-001",
"31": "DOC-001",
"32": "adminRoutes.tsx",
"33": "WholesaleService",
"33": "WholesaleApplyDto",
"34": "B2BService",
"35": "PetProfile.tsx",
"35": "AuthController",
"36": "FaqService",
"37": "راهنمای تست سیستم (Software Testing)",
"38": "Button",
@ -59,47 +47,27 @@
"45": "What You Must Do When Invoked",
"46": "HomeController",
"47": "IngredientsService",
<<<<<<< HEAD
"48": "useCartStore",
=======
"48": "auth.module.ts",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"49": "devDependencies",
"50": "devDependencies",
"51": "BlogsController",
"52": "PrescriptionsService",
"53": "SmartAdvisorService",
<<<<<<< HEAD
"54": "UsersService",
=======
"54": "UsersController",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"55": "UITexts.tsx",
"56": "Orders.tsx",
"57": "Role & Core Objective",
"58": "ContactService",
"59": "compilerOptions",
<<<<<<< HEAD
"60": "PaymentService",
"61": "AdminTransactionFilterDto",
"62": "CreateReviewDto",
"63": "dependencies",
"64": "compilerOptions",
"65": "Media.tsx",
"66": "WikiController",
"67": "PetsController",
"68": "20260526145407_init/migration.sql",
=======
"60": "users.controller.ts",
"61": "payment.service.ts",
"62": "ProductDto",
"63": "dependencies",
"64": "compilerOptions",
"65": "app.e2e-spec.js",
"66": "ApiOperation",
"67": "admin.module.ts",
"66": "AdminQueryDto",
"67": "PetsController",
"68": "PaymentService",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"69": "Required Review Group Closures",
"70": "compilerOptions",
"71": "getPageMetadata",
@ -107,36 +75,28 @@
"73": "Operational Rules & Boundaries",
"74": "WikiController",
"75": "PetsController",
<<<<<<< HEAD
"76": "CreateEBankCheckoutDto",
=======
"76": "AdminService",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"76": ".getDashboardStats",
"77": "seo.module.ts",
"78": "route.ts",
"78": "rss.xml/route.ts",
"79": "🏢 AI Software Agency — Master Orchestration Protocol v3",
"80": "Operational Rules & Boundaries",
"81": "Operational Rules & Boundaries",
"82": "scripts",
"83": "dependencies",
"84": "Role & Core Objective",
"85": "eslint-config-prettier",
"85": "ArchivePage.tsx",
"86": "zibal.service.ts",
"87": "dependencies",
<<<<<<< HEAD
"88": "ProductPage.tsx",
=======
"88": "toPersian",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"88": "NetworkBanner.tsx",
"89": "seed-products.ts",
"90": "useSettingsStore",
"91": "Reconciled Audit Roles & Assignments",
"92": "OrdersService",
"93": "@nestjs/schematics",
"94": "Phase 3.1 — Human Review Preparation and Master Backlog Critique",
"95": "schema.ts",
"95": "blog/[slug]/page.tsx",
"96": "compilerOptions",
"97": "blog/[slug]/page.tsx",
"97": "SettingsService",
"98": "scripts",
"99": "BlogsController",
"100": "Deep Audit Summary Report",
@ -145,19 +105,14 @@
"103": "Comprehensive Change Log",
"104": "Coupons.tsx",
"105": "Operational Rules & Boundaries",
<<<<<<< HEAD
"106": "blog/page.tsx",
"107": "layout.tsx",
=======
"106": "Media.tsx",
"107": "@types/supertest",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"107": "WikiController",
"108": "PrismaService",
"109": "1. Summary of Integrity Repairs Performed",
"110": "Operational Rules & Boundaries",
"111": "Operational Rules & Boundaries",
"112": "Operational Rules & Boundaries",
"113": "CreateReviewDto",
"113": "SmsService",
"114": "AppService",
"115": "UsersService",
"116": "Vazirmatn Changelog",
@ -166,57 +121,43 @@
"119": "compilerOptions",
"120": "compilerOptions",
"121": "backend/README.md",
<<<<<<< HEAD
"122": "videos/page.tsx",
"123": "BlogsService",
=======
"122": "Param",
"122": "AdminService",
"123": "CreateEBankCheckoutDto",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"124": "Repository Map",
"125": "validate_integrity.js",
"126": "admin-panel/package.json",
"127": "Sahel-Font",
<<<<<<< HEAD
"128": "AdminService",
"129": "نقشه جامع پوشش تست‌های سرتاسری (E2E Test Coverage Map) — پروژه Canina",
=======
"128": "AdminController",
"129": "InitiatePaymentDto",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"130": "Sahel-Font",
"131": "Role & Core Objective",
"132": "orchestrate.py",
"133": "backend/package.json",
"134": "app.e2e-spec.js",
"134": "blog/page.tsx",
"135": "graphify reference: extra exports and benchmark",
"136": "Phase 2 Final Quality Gate Summary Report",
"137": "Task Modifications Log",
"138": "Install",
"139": "catalog/page.tsx",
"139": "layout.tsx",
"140": "ErrorBoundary",
"141": "application/package.json",
"142": "start-dev.js",
"143": "generate-openapi.js",
"144": "@testing-library/react",
"145": "@types/node",
"145": "admin.service.ts",
"146": "System Discovery",
"147": "RevalidationService",
"148": "typescript",
"147": "useCartStore",
"148": "wiki/[slug]/page.tsx",
"149": "SmsSettingsPage.tsx",
"150": "Product Requirement Document (PRD)",
<<<<<<< HEAD
"151": "admin.service.ts",
"152": "InitiatePaymentDto",
=======
"151": "auth.service.ts",
"152": ".createCoupon",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"152": "cartStore.ts",
"153": "exclude",
"154": "Baseline Command Plan & Reconciled Command History",
"155": "seo-backfill.ts",
"156": "manual-test-scenarios.md",
"157": "ErrorPages.tsx",
"158": "AuthService",
"159": "with-vpn.sh",
"160": "Architecture Specification",
"161": "Project Health Audit Report",
@ -234,12 +175,16 @@
"173": "Phase 3 Audit Traceability Matrix",
"174": "rebuild_honest_ledger.js",
"175": "validate_evidence_grade.js",
"176": "CreateOrderDto",
"177": "SmsLogQueryDto",
"178": "نقشه جامع پوشش تست‌های سرتاسری (E2E Test Coverage Map) — پروژه Canina",
"179": "PaginationDto",
"180": "API Contract Specification",
"181": "⚙️ Backend Technical Review (05_dev_backend)",
"182": "🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)",
"183": "🚀 SEO & Content Strategy Review (12_seo_content)",
"184": "MetricsController",
"184": "RedisService",
"185": "trust-seals/page.tsx",
"186": "seed-ui-texts.ts",
"187": "seed-wiki.ts",
"188": "update-blog.dto.ts",
@ -251,11 +196,14 @@
"194": "Raw Finding Verification & Disposition Report",
"195": "React + TypeScript + Vite",
"196": "Select.tsx",
"199": "auth.controller.ts",
"197": "wiki/page.tsx",
"198": "catalog/page.tsx",
"199": "RegisterDto",
"200": "application/README.md",
"201": "deploy.sh",
"202": "🔒 Security & Performance Review (09_devops_security)",
"203": "👁️ UX & Persona Interface Review (08_visual_qa)",
"204": "eslint",
"205": "prisma/scientificTerms.ts",
"206": "seed-blogs.ts",
"207": "seed-custom.ts",
@ -274,6 +222,7 @@
"220": "Input.tsx",
"221": "Textarea.tsx",
"222": "admin-panel/tsconfig.json",
"223": "@types/node",
"224": "jest",
"225": "next.config.ts",
"226": "Shabnam Font README",
@ -281,16 +230,9 @@
"228": "rules/graphify.md",
"229": ".agents/workflows/graphify.md",
"230": "instructions.md",
"231": "typescript",
"232": "@eslint/js",
<<<<<<< HEAD
"234": "20260526160916_add_ui_texts_and_scientific_terms/migration.sql",
=======
"233": "eslint-plugin-prettier",
"234": "WholesaleApplyDto",
"235": "@nestjs/cli",
"236": "@nestjs/testing",
"237": "app/page.tsx",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"233": "globals",
"238": "prisma",
"239": "source-map-support",
"240": "supertest",
@ -349,7 +291,6 @@
"293": "Staging Docker Compose",
"294": "ZibalService",
"295": "eslint-plugin-react-refresh",
"296": "tailwindcss",
"297": "ZibalEBankService",
"298": ".initiateOrderPayment",
"299": "@tailwindcss/postcss",
@ -365,12 +306,7 @@
"311": "@types/multer",
"312": "@types/passport-jwt",
"313": "Modal.tsx",
<<<<<<< HEAD
=======
"314": "typescript-eslint",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"315": "typescript",
"316": "ts-jest",
"317": "revalidate/route.ts",
"318": "MaskableField.tsx",
"319": "eslint-config-next"

File diff suppressed because one or more lines are too long

View File

@ -3,25 +3,37 @@
"1": "app.module.ts",
"2": "SmsService",
"3": "ProductService",
"4": "SafeImage.tsx",
"4": "utils.ts",
"5": "CmsController",
"6": "tickets.controller.ts",
"7": "pets/pets.controller.ts",
"8": "admin.module.ts",
"7": "Button.tsx",
"8": "ReportsController",
"9": "devDependencies",
<<<<<<< HEAD
"10": "ReviewsController",
"11": "MediaSelector.tsx",
"12": "index.ts",
"13": "app-audit-verification.e2e-spec.js",
"14": "useSettingsStore",
=======
"10": "ReviewsService",
"11": "Spinner.tsx",
"11": "MediaSelector.tsx",
"12": "PetProfile.tsx",
"13": "app-audit-verification.e2e-spec.js",
"14": "lib/services/api.ts",
"14": "UserDashboard.tsx",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"15": "src/services/api.ts",
"16": "DoctorQueryDto",
"17": "schema.ts",
"17": "UserDashboard.tsx",
"18": "JwtAuthGuard",
"19": "ProductsController",
"19": "ProductsService",
"20": "CreateVideoDto",
<<<<<<< HEAD
"21": "Button.tsx",
=======
"21": "ProductPage.tsx",
"22": "UserDashboard.tsx",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"22": "lib/services/api.ts",
"23": "MenuService",
"24": "BE-001",
"25": "FE-001",
@ -32,42 +44,62 @@
"30": "DEVOPS-001",
"31": "DOC-001",
"32": "adminRoutes.tsx",
"33": "WholesaleApplyDto",
"33": "WholesaleService",
"34": "B2BService",
"35": "AuthController",
"35": "PetProfile.tsx",
"36": "FaqService",
"37": "راهنمای تست سیستم (Software Testing)",
"38": "Button.tsx",
"38": "Button",
"39": "CategoriesController",
"40": "MediaController",
"41": "What You Must Do When Invoked",
"42": "SslController",
"43": "BannersService",
"44": "TestimonialsController",
"44": "TestimonialsService",
"45": "What You Must Do When Invoked",
"46": "api",
"46": "HomeController",
"47": "IngredientsService",
"48": "cartStore.ts",
<<<<<<< HEAD
"48": "useCartStore",
=======
"48": "auth.module.ts",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"49": "devDependencies",
"50": "devDependencies",
"51": "BlogsController",
"52": "PrescriptionsController",
"53": "SmartAdvisorController",
"52": "PrescriptionsService",
"53": "SmartAdvisorService",
<<<<<<< HEAD
"54": "UsersService",
=======
"54": "UsersController",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"55": "UITexts.tsx",
"56": "Orders.tsx",
"57": "Role & Core Objective",
"58": "ContactService",
"59": "compilerOptions",
"60": "Blogs.tsx",
"61": "payment.controller.ts",
"62": "ProductsService",
<<<<<<< HEAD
"60": "PaymentService",
"61": "AdminTransactionFilterDto",
"62": "CreateReviewDto",
"63": "dependencies",
"64": "compilerOptions",
"65": "Media.tsx",
"66": "WikiController",
"67": "PetsController",
"68": "20260526145407_init/migration.sql",
=======
"60": "users.controller.ts",
"61": "payment.service.ts",
"62": "ProductDto",
"63": "dependencies",
"64": "compilerOptions",
"65": "app.e2e-spec.js",
"66": "AdminQueryDto",
"67": "PetsController",
"68": "20260526145407_init/migration.sql",
"66": "ApiOperation",
"67": "admin.module.ts",
"68": "PaymentService",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"69": "Required Review Group Closures",
"70": "compilerOptions",
"71": "getPageMetadata",
@ -75,7 +107,11 @@
"73": "Operational Rules & Boundaries",
"74": "WikiController",
"75": "PetsController",
"76": "shop/page.tsx",
<<<<<<< HEAD
"76": "CreateEBankCheckoutDto",
=======
"76": "AdminService",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"77": "seo.module.ts",
"78": "route.ts",
"79": "🏢 AI Software Agency — Master Orchestration Protocol v3",
@ -87,16 +123,20 @@
"85": "eslint-config-prettier",
"86": "zibal.service.ts",
"87": "dependencies",
"88": "useSettingsStore",
<<<<<<< HEAD
"88": "ProductPage.tsx",
=======
"88": "toPersian",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"89": "seed-products.ts",
"90": "HomeClient.tsx",
"90": "useSettingsStore",
"91": "Reconciled Audit Roles & Assignments",
"92": "OrdersService",
"93": "@nestjs/schematics",
"94": "Phase 3.1 — Human Review Preparation and Master Backlog Critique",
"95": "blog/[slug]/page.tsx",
"95": "schema.ts",
"96": "compilerOptions",
"97": "prettier",
"97": "blog/[slug]/page.tsx",
"98": "scripts",
"99": "BlogsController",
"100": "Deep Audit Summary Report",
@ -105,59 +145,78 @@
"103": "Comprehensive Change Log",
"104": "Coupons.tsx",
"105": "Operational Rules & Boundaries",
"106": "Reports.tsx",
<<<<<<< HEAD
"106": "blog/page.tsx",
"107": "layout.tsx",
=======
"106": "Media.tsx",
"107": "@types/supertest",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"108": "PrismaService",
"109": "1. Summary of Integrity Repairs Performed",
"110": "Operational Rules & Boundaries",
"111": "Operational Rules & Boundaries",
"112": "Operational Rules & Boundaries",
"113": "reviews.controller.ts",
"113": "CreateReviewDto",
"114": "AppService",
"115": "@types/react-dom",
"115": "UsersService",
"116": "Vazirmatn Changelog",
"117": "Vazirmatn Font فونت وزیرمتن",
"118": "Operational Rules & Boundaries",
"119": "compilerOptions",
"120": "compilerOptions",
"121": "backend/README.md",
"122": "AuthService",
<<<<<<< HEAD
"122": "videos/page.tsx",
"123": "BlogsService",
=======
"122": "Param",
"123": "CreateEBankCheckoutDto",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"124": "Repository Map",
"125": "validate_integrity.js",
"126": "admin-panel/package.json",
"127": "Sahel-Font",
<<<<<<< HEAD
"128": "AdminService",
"129": "RouteErrorBoundary",
"129": "نقشه جامع پوشش تست‌های سرتاسری (E2E Test Coverage Map) — پروژه Canina",
=======
"128": "AdminController",
"129": "InitiatePaymentDto",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"130": "Sahel-Font",
"131": "Role & Core Objective",
"132": "orchestrate.py",
"133": "backend/package.json",
"134": "blog/page.tsx",
"134": "app.e2e-spec.js",
"135": "graphify reference: extra exports and benchmark",
"136": "Phase 2 Final Quality Gate Summary Report",
"137": "Task Modifications Log",
"138": "Install",
"139": "layout.tsx",
"139": "catalog/page.tsx",
"140": "ErrorBoundary",
"141": "application/package.json",
"142": "start-dev.js",
"143": "generate-openapi.js",
"144": "@testing-library/react",
"145": "admin.service.ts",
"145": "@types/node",
"146": "System Discovery",
"147": "RevalidationService",
"148": "wiki/[slug]/page.tsx",
"148": "typescript",
"149": "SmsSettingsPage.tsx",
"150": "Product Requirement Document (PRD)",
<<<<<<< HEAD
"151": "admin.service.ts",
"152": "InitiatePaymentDto",
=======
"151": "auth.service.ts",
"152": "trust-seals/page.tsx",
"152": ".createCoupon",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"153": "exclude",
"154": "Baseline Command Plan & Reconciled Command History",
"155": "seo-backfill.ts",
"156": "@nestjs/swagger",
"156": "manual-test-scenarios.md",
"157": "ErrorPages.tsx",
"158": "class-transformer",
"159": "with-vpn.sh",
"160": "Architecture Specification",
"161": "Project Health Audit Report",
@ -175,16 +234,12 @@
"173": "Phase 3 Audit Traceability Matrix",
"174": "rebuild_honest_ledger.js",
"175": "validate_evidence_grade.js",
"176": "helmet",
"177": "js-yaml",
"178": "@nestjs/core",
"179": "PaginationDto",
"180": "API Contract Specification",
"181": "⚙️ Backend Technical Review (05_dev_backend)",
"182": "🎨 Frontend & Admin Panel Technical Review (06_dev_frontend)",
"183": "🚀 SEO & Content Strategy Review (12_seo_content)",
"184": "MetricsController",
"185": "@nestjs/jwt",
"186": "seed-ui-texts.ts",
"187": "seed-wiki.ts",
"188": "update-blog.dto.ts",
@ -196,14 +251,11 @@
"194": "Raw Finding Verification & Disposition Report",
"195": "React + TypeScript + Vite",
"196": "Select.tsx",
"197": "@nestjs/throttler",
"198": "passport",
"199": "auth.controller.ts",
"200": "application/README.md",
"201": "deploy.sh",
"202": "🔒 Security & Performance Review (09_devops_security)",
"203": "👁️ UX & Persona Interface Review (08_visual_qa)",
"204": "reflect-metadata",
"205": "prisma/scientificTerms.ts",
"206": "seed-blogs.ts",
"207": "seed-custom.ts",
@ -222,7 +274,6 @@
"220": "Input.tsx",
"221": "Textarea.tsx",
"222": "admin-panel/tsconfig.json",
"223": "swagger-ui-express",
"224": "jest",
"225": "next.config.ts",
"226": "Shabnam Font README",
@ -230,13 +281,16 @@
"228": "rules/graphify.md",
"229": ".agents/workflows/graphify.md",
"230": "instructions.md",
"231": "@eslint/eslintrc",
"232": "@eslint/js",
"233": "eslint-plugin-prettier",
<<<<<<< HEAD
"234": "20260526160916_add_ui_texts_and_scientific_terms/migration.sql",
=======
"233": "eslint-plugin-prettier",
"234": "WholesaleApplyDto",
"235": "@nestjs/cli",
"236": "@nestjs/testing",
"237": "eslint",
"237": "app/page.tsx",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"238": "prisma",
"239": "source-map-support",
"240": "supertest",
@ -305,16 +359,19 @@
"303": "@types/express",
"304": "@types/jest",
"305": "@types/react",
"306": "globals",
"307": "@types/js-yaml",
"308": "vitest",
"309": "axios",
"310": "tailwindcss",
"311": "@types/multer",
"312": "@types/passport-jwt",
"313": "MenuManager.tsx",
"313": "Modal.tsx",
<<<<<<< HEAD
=======
"314": "typescript-eslint",
>>>>>>> cf9ae234b9938be85c833ab98741a4590f9cfe13
"315": "typescript",
"316": "ts-jest",
"317": "revalidate/route.ts",
"318": "MaskableField.tsx"
"318": "MaskableField.tsx",
"319": "eslint-config-next"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff