fix: resolve tooltip opacity, species icons, catalog pagination and swipe, image frames and description collapse
Some checks failed
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 7s
Deploy Canina / deploy (push) Has been cancelled

This commit is contained in:
parsa aghaei 2026-08-26 12:00:55 +03:30
parent 0b52f9e9e4
commit 9daaa22677
17 changed files with 3050 additions and 2970 deletions

View File

@ -129,13 +129,13 @@ const ArchiveProductCard: React.FC<{ product: Product }> = ({ product }) => {
</div>
)}
<div className="aspect-square bg-medical-gray-50 flex items-center justify-center p-8 overflow-hidden relative">
<div className="aspect-square bg-medical-gray-50 flex items-center justify-center p-2 overflow-hidden relative">
<SafeImage
src={product.image}
alt={product.name}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw)"
className="w-full h-full group-hover:scale-110 transition-transform duration-700"
imgClassName="object-contain"
className="w-full h-full group-hover:scale-105 transition-transform duration-700"
imgClassName="w-full h-full object-contain"
/>
<div className="absolute bottom-4 left-4 flex gap-2">
{(product.suitableFor === "سگ" || product.suitableFor === "هر دو") && (

View File

@ -4,7 +4,7 @@ import { Product } from "../lib/data/products";
import { usePetStore } from "../lib/store/usePetStore";
import { productService } from "../lib/services/productService";
import { motion } from "motion/react";
import { Eye, ShoppingCart, Heart, AlertCircle, Sparkles } from "lucide-react";
import { Eye, ShoppingCart, Heart, AlertCircle, Sparkles, Dog, Cat } from "lucide-react";
import SafeImage from "./SafeImage";
import { ProductCardSkeleton } from "./Skeleton";
import Link from "next/link";
@ -68,14 +68,28 @@ function ProductCard({ product }: { product: Product }) {
)}
{/* Image Container */}
<div className="relative aspect-[16/11] sm:aspect-[4/3] bg-medical-gray-50 flex items-center justify-center overflow-hidden p-3 sm:p-5">
<div className="relative aspect-[16/11] sm:aspect-[4/3] bg-medical-gray-50 flex items-center justify-center overflow-hidden p-1 sm:p-2">
<SafeImage
src={product.image}
alt={nameFa}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw)"
className="w-full h-full group-hover:scale-105 transition-transform duration-500"
imgClassName="object-contain max-h-[130px] sm:max-h-[170px] mx-auto"
imgClassName="object-contain w-full h-full max-h-[160px] sm:max-h-[200px] mx-auto"
/>
{/* Species Badges (Dog / Cat) */}
<div className="absolute bottom-2 left-2 flex gap-1 z-10">
{(product.suitableFor === "سگ" || product.suitableFor === "هر دو") && (
<div className="w-6 h-6 bg-white/90 backdrop-blur-xs rounded-md flex items-center justify-center text-medical-gray-500 border border-medical-gray-200 shadow-2xs" title="مناسب برای سگ">
<Dog className="w-3.5 h-3.5 text-canina-blue" />
</div>
)}
{(product.suitableFor === "گربه" || product.suitableFor === "هر دو") && (
<div className="w-6 h-6 bg-white/90 backdrop-blur-xs rounded-md flex items-center justify-center text-medical-gray-500 border border-medical-gray-200 shadow-2xs" title="مناسب برای گربه">
<Cat className="w-3.5 h-3.5 text-canina-blue" />
</div>
)}
</div>
{/* Hover Actions for desktop */}
<div className="hidden sm:flex absolute inset-0 bg-canina-blue/60 opacity-0 group-hover:opacity-100 backdrop-blur-xs transition-all duration-300 items-center justify-center gap-3 z-20">
<div

View File

@ -283,23 +283,28 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
</div>
{/* Global Single Grid Layout - Aligns Sticky Buy Box at the Top */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-start w-full">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 sm:gap-8 lg:gap-12 items-start w-full">
{/* Right/Main Content Column (lg:col-span-8) */}
<div className="w-full lg:col-span-8 space-y-12">
<div className="w-full lg:col-span-8 space-y-4 sm:space-y-8 lg:space-y-12">
{/* Above the Fold Grid */}
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 md:gap-12 items-center md:items-start text-center md:text-right font-vazir w-full" dir="rtl">
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 sm:gap-8 md:gap-12 items-center md:items-start text-center md:text-right font-vazir w-full" dir="rtl">
{/* Right Column: Product Image Frame & Gallery (md:col-span-5) */}
<div className="md:col-span-5 w-full mx-auto max-w-sm md:max-w-none space-y-4">
<div className="md:col-span-5 w-full mx-auto max-w-sm md:max-w-none space-y-3 sm:space-y-4">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
onClick={() => setIsImageZoomOpen(true)}
className="bg-white rounded-[2.5rem] border border-medical-gray-200 shadow-xl relative group overflow-hidden h-[300px] sm:h-[380px] flex items-center justify-center p-6 sm:p-8 cursor-zoom-in"
className="bg-white rounded-2xl sm:rounded-[2.5rem] border border-medical-gray-200 shadow-xl relative group overflow-hidden h-[260px] sm:h-[380px] flex items-center justify-center p-2 sm:p-4 cursor-zoom-in"
title="کلیک برای مشاهده تصویر بزرگتر"
>
<div className="absolute top-4 left-4 sm:top-6 sm:left-6 flex flex-col gap-1.5 sm:gap-2 z-10 items-start">
{product.specialBadge && (
<div className="px-2.5 py-1 bg-medical-gray-900 text-white rounded-full text-[9px] sm:text-[10px] font-black uppercase tracking-widest font-vazir whitespace-nowrap shadow-sm">
{product.specialBadge}
</div>
)}
{isOutOfStock ? (
<div className="px-2.5 py-1 bg-rose-500 text-white rounded-full text-[9px] sm:text-[10px] font-black uppercase tracking-widest flex items-center gap-1 font-vazir shadow-sm">
ناموجود در انبار
@ -334,7 +339,7 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
priority={true}
sizes="(max-width: 768px) 100vw, 400px"
className="w-full h-full"
imgClassName="max-h-[240px] sm:max-h-[300px] w-auto object-contain drop-shadow-2xl group-hover:scale-105 transition-transform duration-700 mx-auto my-auto"
imgClassName="w-full h-full object-contain drop-shadow-2xl group-hover:scale-105 transition-transform duration-700 mx-auto my-auto"
/>
<div className="absolute bottom-4 right-4 bg-black/60 backdrop-blur-md text-white px-3 py-1 rounded-full text-[10px] font-black flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<span>بزرگنمایی</span>
@ -435,20 +440,20 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
</div>
{/* ─── Symptoms / Therapeutic Indications Section ─── */}
{fullProduct.symptoms && fullProduct.symptoms.length > 0 && (
<section className="bg-canina-blue/[0.03] border border-canina-blue/10 rounded-[2rem] p-6 md:p-8 space-y-4">
<div className="flex items-center gap-3">
<div className="w-9 h-9 bg-canina-blue/10 text-canina-blue rounded-xl flex items-center justify-center">
<Stethoscope className="w-5 h-5" />
<section className="bg-canina-blue/[0.03] border border-canina-blue/10 rounded-2xl sm:rounded-[2rem] p-4 sm:p-6 md:p-8 space-y-3 sm:space-y-4">
<div className="flex items-center gap-2.5 sm:gap-3">
<div className="w-8 h-8 sm:w-9 sm:h-9 bg-canina-blue/10 text-canina-blue rounded-xl flex items-center justify-center">
<Stethoscope className="w-4 h-4 sm:w-5 sm:h-5" />
</div>
<h3 className="text-base font-black text-medical-gray-900 font-vazir">علائم و موارد درمانی مرتبط</h3>
<h3 className="text-sm sm:text-base font-black text-medical-gray-900 font-vazir">علائم و موارد درمانی مرتبط</h3>
</div>
<p className="text-[12px] text-medical-gray-400 font-vazir font-bold">این محصول برای رفع علائم زیر توسط دامپزشکان توصیه میشود:</p>
<div className="flex flex-wrap gap-2">
<p className="text-[11px] sm:text-[12px] text-medical-gray-400 font-vazir font-bold">این محصول برای رفع علائم زیر توسط دامپزشکان توصیه میشود:</p>
<div className="flex flex-wrap gap-1.5 sm:gap-2">
{fullProduct.symptoms.map((symptom: string, idx: number) => (
<a
key={idx}
href={`/shop?symptom=${encodeURIComponent(symptom)}`}
className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-full text-[11px] font-bold bg-white border border-canina-blue/20 text-canina-blue hover:bg-canina-blue hover:text-white hover:border-canina-blue transition-all shadow-sm cursor-pointer font-vazir whitespace-nowrap"
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[10px] sm:text-[11px] font-bold bg-white border border-canina-blue/20 text-canina-blue hover:bg-canina-blue hover:text-white hover:border-canina-blue transition-all shadow-2xs cursor-pointer font-vazir whitespace-nowrap"
>
<span className="w-1.5 h-1.5 rounded-full bg-current opacity-60" />
{symptom}
@ -460,24 +465,24 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
{/* Key Benefits Section */}
{fullProduct.keyBenefits && fullProduct.keyBenefits.length > 0 && (
<section className="space-y-8 pt-6 border-t border-medical-gray-100">
<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">
<Star className="w-6 h-6" />
<section className="space-y-4 sm:space-y-8 pt-4 sm:pt-6 border-t border-medical-gray-100">
<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-xl sm:rounded-2xl flex items-center justify-center shadow-lg shrink-0">
<Star className="w-5 h-5 sm:w-6 sm:h-6" />
</div>
<h3 className="text-3xl font-black text-medical-gray-900 italic font-vazir">چرا {fullProduct.name}؟</h3>
<h3 className="text-xl sm:text-3xl font-black text-medical-gray-900 italic font-vazir">چرا {fullProduct.name}؟</h3>
</div>
<div className="grid md:grid-cols-3 gap-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 sm:gap-6">
{fullProduct.keyBenefits.map((benefit, i) => {
const Icon = ICON_MAP[benefit.icon] || CheckCircle2;
return (
<div key={i} className="bg-white border border-medical-gray-200 rounded-[2.5rem] p-8 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all group overflow-hidden relative">
<div key={i} className="bg-white border border-medical-gray-200 rounded-2xl sm:rounded-[2.5rem] p-4 sm:p-8 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all group overflow-hidden relative">
<div className="absolute -top-10 -left-10 w-24 h-24 bg-canina-blue/5 rounded-full group-hover:scale-150 transition-transform duration-700" />
<div className="w-12 h-12 bg-medical-gray-50 rounded-2xl flex items-center justify-center text-canina-blue mb-6 group-hover:bg-canina-blue group-hover:text-white transition-all">
<Icon className="w-6 h-6" />
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-medical-gray-50 rounded-xl sm:rounded-2xl flex items-center justify-center text-canina-blue mb-3 sm:mb-6 group-hover:bg-canina-blue group-hover:text-white transition-all">
<Icon className="w-5 h-5 sm:w-6 sm:h-6" />
</div>
<h4 className="text-xl font-black text-medical-gray-900 mb-2 font-vazir">{benefit.title}</h4>
<p className="text-sm text-medical-gray-500 font-bold font-vazir">{benefit.description}</p>
<h4 className="text-base sm:text-xl font-black text-medical-gray-900 mb-1 sm:mb-2 font-vazir">{benefit.title}</h4>
<p className="text-xs sm:text-sm text-medical-gray-500 font-bold font-vazir text-justify">{benefit.description}</p>
</div>
);
})}
@ -487,30 +492,34 @@ 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-[2rem] sm:rounded-[2.5rem] p-5 sm:p-8 md:p-10 shadow-md space-y-4 sm:space-y-6">
<section className="bg-white border border-medical-gray-200 rounded-[1.5rem] sm:rounded-[2.5rem] p-4 sm:p-8 md:p-10 shadow-md space-y-3 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">
<div className="w-9 h-9 sm:w-12 sm:h-12 bg-canina-blue text-white rounded-xl sm: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-xl sm:text-2xl font-black text-medical-gray-900 italic font-vazir">توضیحات و معرفی محصول</h3>
<h3 className="text-lg sm:text-2xl font-black text-medical-gray-900 italic font-vazir">توضیحات و معرفی محصول</h3>
</div>
<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">
<div className="text-xs sm:text-base text-medical-gray-600 font-medium leading-relaxed sm:leading-loose font-vazir space-y-2 sm:space-y-3 pt-2 sm: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;
const descText = fullProduct.description.trim();
const paragraphs = descText.split('\n').filter(Boolean);
const isLongText = descText.length > 180 || paragraphs.length > 1;
return (
<>
<div className="space-y-2">
{displayedParagraphs.map((para, pIdx) => (
<p key={pIdx}>{para.trim()}</p>
))}
<div className="space-y-2 text-justify">
{isLongText && !isFullDescriptionOpen ? (
<p>
{descText.slice(0, 180)}...
</p>
) : (
paragraphs.map((para, pIdx) => (
<p key={pIdx}>{para.trim()}</p>
))
)}
</div>
{shouldTruncate && (
{isLongText && (
<div className="pt-2 flex justify-center">
<button
type="button"
@ -538,13 +547,13 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
fullProduct.nameFa?.includes('کرم') ||
fullProduct.nameFa?.includes('قطره تمیزکننده') ||
fullProduct.nameFa?.includes('اسپری')) && (
<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" />
<section className="bg-white rounded-2xl sm:rounded-[2.5rem] border border-medical-gray-200 p-3.5 sm:p-6 md:p-8 shadow-md space-y-4 sm:space-y-6">
<div className="flex items-center gap-2.5 sm:gap-4">
<div className="w-9 h-9 sm:w-12 sm:h-12 bg-canina-blue text-white rounded-xl sm:rounded-2xl flex items-center justify-center shadow-lg shrink-0">
<Clock className="w-4 h-4 sm:w-6 sm:h-6" />
</div>
<div>
<h3 className="text-base sm:text-2xl font-black text-medical-gray-900 italic font-vazir whitespace-nowrap">
<h3 className="text-sm sm:text-xl font-black text-medical-gray-900 italic font-vazir whitespace-nowrap">
{activePet ? `ماشین حساب هوشمند مصرف برای ${activePet.name}` : 'ماشین حساب هوشمند مصرف'}
</h3>
<p className="text-[10px] sm:text-xs text-medical-gray-400 font-bold font-vazir mt-0.5 sm:mt-1">
@ -555,14 +564,14 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
</div>
</div>
<div className="grid md:grid-cols-2 gap-6 sm:gap-8 items-center">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6 items-center">
{/* Manual Slider Inputs */}
<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="bg-medical-gray-50 rounded-xl sm:rounded-[2rem] p-3.5 sm:p-5 border border-medical-gray-100 space-y-4">
<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-base sm:text-lg font-black text-canina-blue font-vazir">{toPersian(weight)} کگ</span>
<span className="text-sm sm:text-base font-black text-canina-blue font-vazir">{toPersian(weight)} کگ</span>
</div>
<input
type="range"
@ -577,15 +586,15 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
{activePet ? `سن ${activePet.name}` : 'سن حیوان'}
</span>
<div className="flex gap-2">
<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>
<button onClick={() => setPetType("young")} className={`flex-1 py-2 sm:py-2.5 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 sm:py-2.5 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-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="bg-canina-blue rounded-xl sm:rounded-[2rem] p-4 sm:p-6 text-white border-b-6 border-white/20 shadow-2xl flex flex-col justify-between h-full min-h-[180px]">
<div className="flex items-center justify-between mb-3 opacity-90 border-b border-white/10 pb-2.5 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" />
@ -602,28 +611,28 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
</span>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-2 gap-3">
<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 className="text-xl sm:text-2xl font-black font-vazir text-white">{toPersian(calculation.duration)} روز</div>
<div className="text-[10px] font-bold text-white/80 font-vazir whitespace-nowrap mt-0.5">مدت تامین یک بسته</div>
</div>
<div className="text-left">
<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 className="text-lg sm:text-xl font-black font-vazir text-white">{toPersian(calculation.dailyDose)}</div>
<div className="text-[10px] font-bold text-white/80 font-vazir whitespace-nowrap mt-0.5">دوز روزانه ({calculation.unit === 'g' ? 'گرم' : calculation.unit})</div>
</div>
</div>
<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>
<div className="mt-3 pt-3 border-t border-white/10 flex items-center justify-between gap-2">
<span className="text-[10px] sm: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/70 font-bold font-vazir mt-3 leading-relaxed">
<p className="text-[9px] sm:text-[10px] text-white/70 font-bold font-vazir mt-2 leading-relaxed">
{activePet
? `* دوز دقیق بر اساس وزن ${activePet.name} (${toPersian(weight)} ک‌گ) محاسبه شده است؛ برای ماندگاری کامل دوره، خرید تعداد بسته‌های پیشنهادی توصیه می‌شود.`
: '* دوز دقیق بر اساس وزن پت محاسبه شده است؛ برای ماندگاری کامل دوره، خرید تعداد بسته‌های پیشنهادی توصیه می‌شود.'}
? `* دوز دقیق بر اساس وزن ${activePet.name} (${toPersian(weight)} ک‌گ) محاسبه شده است.`
: '* دوز دقیق بر اساس وزن پت محاسبه شده است.'}
</p>
</div>
</div>
@ -631,12 +640,12 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
)}
{/* Ingredients Section */}
<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" />
<section className="bg-white rounded-2xl sm:rounded-[2.5rem] border border-medical-gray-200 p-4 sm:p-6 md:p-8 shadow-md space-y-4 sm:space-y-6">
<div className="flex items-center gap-2.5 sm:gap-4 border-b border-medical-gray-100 pb-3 sm:pb-4">
<div className="w-9 h-9 sm:w-12 sm:h-12 bg-canina-blue text-white rounded-xl sm:rounded-2xl flex items-center justify-center shadow-lg shrink-0">
<FlaskConical className="w-4 h-4 sm:w-6 sm:h-6" />
</div>
<h3 className="text-xl sm:text-2xl md:text-3xl font-black text-medical-gray-900 italic font-vazir">ترکیبات و آنالیز علمی</h3>
<h3 className="text-lg sm:text-2xl font-black text-medical-gray-900 italic font-vazir">ترکیبات و آنالیز علمی</h3>
</div>
<div className="space-y-6 sm:space-y-8">

View File

@ -53,18 +53,16 @@ export default function SafeImage({
if (!src || error) {
return (
<div className={cn(
"flex flex-col items-center justify-center bg-medical-gray-50/90 border border-medical-gray-100 p-2 text-center w-full h-full rounded-2xl relative overflow-hidden select-none aspect-square",
"flex flex-col items-center justify-center bg-medical-gray-50 border border-medical-gray-200/80 p-3 text-center w-full h-full rounded-2xl relative overflow-hidden select-none aspect-square",
className
)}>
<Image
src="/assets/images/canina-product-placeholder.png"
alt={alt}
width={240}
height={240}
className="max-h-[85%] w-auto object-contain opacity-70"
unoptimized
/>
<span className="text-[9px] font-black text-canina-blue/80 font-vazir leading-tight mt-1 line-clamp-1 bg-white/80 px-2 py-0.5 rounded-md shadow-xs">
<div className="w-12 h-12 sm:w-14 sm:h-14 rounded-2xl bg-canina-blue/10 border border-canina-blue/20 text-canina-blue flex items-center justify-center font-black text-lg font-mono shadow-xs mb-2">
CP
</div>
<span className="text-[11px] font-black text-medical-gray-800 font-vazir leading-tight">
Canina Pharma
</span>
<span className="text-[8px] font-bold text-canina-blue font-vazir leading-tight mt-1 bg-white border border-medical-gray-200 px-2 py-0.5 rounded-full shadow-2xs">
{fallbackText}
</span>
</div>

View File

@ -92,15 +92,15 @@ export default function Tooltip({ termKey, children, onWikiNavigate }: TooltipPr
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }}
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}`}
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-2 border-medical-gray-300/80 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 shrink-0" />
<Info className="w-4 h-4 shrink-0 text-canina-blue" />
<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>
<p className="text-medical-gray-600 text-xs leading-relaxed mb-4 font-bold">
<p className="text-medical-gray-800 text-xs leading-relaxed mb-4 font-bold text-justify">
{termData.definition}
</p>
{termData.wikiId && (

View File

@ -128,15 +128,18 @@ export default function CatalogPageSpread({
</p>
</div>
{/* Hero Render Frame */}
{/* Hero Render Frame: Brand Identity */}
<div className="pt-2 flex justify-center">
<div className="w-36 h-36 md:w-44 md:h-44 rounded-2xl bg-gradient-to-b from-white/10 to-white/5 border border-white/15 backdrop-blur-md p-3 flex flex-col items-center justify-center shadow-2xl relative">
<SafeImage
src="/assets/images/hero-section-image.png"
alt="Canina Pharma Germany"
className="w-full h-full"
imgClassName="object-contain filter drop-shadow-xl"
/>
<div className="w-40 h-40 md:w-48 md:h-48 rounded-2xl bg-gradient-to-b from-white/10 to-white/5 border border-white/15 backdrop-blur-md p-4 flex flex-col items-center justify-center shadow-2xl relative">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-amber-400 to-amber-500 text-slate-950 flex items-center justify-center font-black text-3xl font-mono shadow-xl mb-2">
CP
</div>
<span className="text-xl font-black font-lalezar text-white tracking-wide">
Canina Pharma
</span>
<span className="text-[9px] text-amber-300 font-bold uppercase tracking-widest font-mono mt-0.5">
Germany Est. 1984
</span>
<div className="absolute -bottom-2.5 bg-gradient-to-r from-amber-400 to-amber-500 text-slate-950 font-black text-[9px] px-3 py-0.5 rounded-full shadow-md">
Pharma Grade Certified
</div>

View File

@ -46,9 +46,7 @@ export default function FlipbookCatalog({ products }: FlipbookCatalogProps) {
// 1. Build Category Metadata with dynamic starting pages
const categoryMetaList: CategoryMeta[] = useMemo(() => {
let currentStartPage = 3; // Page 1 = Cover, Page 2 = TOC
const cats: { id: string; name: string; color: string; lightBg: string; borderBg: string; icon: React.ElementType; tagline: string; filterKey: string }[] = [
const cats: { id: string; name: string; color: string; lightBg: string; borderBg: string; icon: React.ElementType; tagline: string; matcher: (p: Product) => boolean }[] = [
{
id: "joints",
name: "مفاصل، استخوان و اسکلت",
@ -57,7 +55,7 @@ export default function FlipbookCatalog({ products }: FlipbookCatalogProps) {
borderBg: "border-sky-200",
icon: Activity,
tagline: "بازسازی غضروف، هیدروکسی آپاتیت طبیعی و تراکم استخوان",
filterKey: "مفاصل و استخوان"
matcher: (p: Product) => (p.category || '').includes("مفاصل") || p.categorySlug === 'joints'
},
{
id: "special-care",
@ -67,7 +65,7 @@ export default function FlipbookCatalog({ products }: FlipbookCatalogProps) {
borderBg: "border-teal-200",
icon: Sparkles,
tagline: "تکنولوژی نقره میکروسیلور، اسیدهای چرب امگا و ترمیم بیولوژیک",
filterKey: "مراقبت‌های ویژه (پوست، دندان و چشم)"
matcher: (p: Product) => (p.category || '').includes("پوست") || (p.category || '').includes("مراقبت") || p.categorySlug === 'special-care'
},
{
id: "immune",
@ -77,7 +75,7 @@ export default function FlipbookCatalog({ products }: FlipbookCatalogProps) {
borderBg: "border-emerald-200",
icon: ShieldCheck,
tagline: "خمیر آغوز متمرکز (Colostrum) و تقویت فوری ایمونوگلوبولین‌ها",
filterKey: "تقویت سیستم ایمنی و گوارش"
matcher: (p: Product) => (p.category || '').includes("ایمنی") || (p.category || '').includes("گوارش") || p.categorySlug === 'immune'
},
{
id: "vitamins",
@ -87,7 +85,7 @@ export default function FlipbookCatalog({ products }: FlipbookCatalogProps) {
borderBg: "border-amber-200",
icon: Zap,
tagline: "تامین عناصر کمیاب، هموگلوبین و توان فیزیکی پت‌ها",
filterKey: "ویتامین‌ها و انرژی‌بخش‌ها"
matcher: (p: Product) => (p.category || '').includes("ویتامین") || (p.category || '').includes("تقویت عمومی") || (p.category || '').includes("انرژی") || p.categorySlug === 'energy' || p.categorySlug === 'general'
},
{
id: "nutrition",
@ -97,14 +95,22 @@ export default function FlipbookCatalog({ products }: FlipbookCatalogProps) {
borderBg: "border-purple-200",
icon: Baby,
tagline: "شیرهای جایگزین مادر با پروتئین بالا و فرنی‌های تغذیه تکمیلی",
filterKey: "تغذیه تخصصی"
matcher: (p: Product) => (p.category || '').includes("تغذیه") || (p.category || '').includes("مکمل") || p.categorySlug === 'nutrition' || p.categorySlug === 'supplements' || (!p.category && !p.categorySlug)
}
];
let currentStartPage = 3; // Page 1 = Cover, Page 2 = TOC
return cats.map(cat => {
const count = products.filter(p => p.category === cat.filterKey || (cat.id === "vitamins" && p.category === "تقویت عمومی")).length;
const count = products.filter(cat.matcher).length;
const meta: CategoryMeta = {
...cat,
id: cat.id,
name: cat.name,
color: cat.color,
lightBg: cat.lightBg,
borderBg: cat.borderBg,
icon: cat.icon,
tagline: cat.tagline,
startPage: currentStartPage
};
currentStartPage += Math.max(count, 1);
@ -117,16 +123,21 @@ export default function FlipbookCatalog({ products }: FlipbookCatalogProps) {
const list: { product: Product; categoryMeta: CategoryMeta; pageNum: number }[] = [];
let pageCounter = 3;
const matchedIds = new Set<string>();
categoryMetaList.forEach(cat => {
const catProds = products.filter(p =>
p.category === (cat.id === "joints" ? "مفاصل و استخوان" :
cat.id === "special-care" ? "مراقبت‌های ویژه (پوست، دندان و چشم)" :
cat.id === "immune" ? "تقویت سیستم ایمنی و گوارش" :
cat.id === "vitamins" ? (p.category === "ویتامین‌ها و انرژی‌بخش‌ها" || p.category === "تقویت عمومی") :
p.category === "تغذیه تخصصی")
);
const matcher = (p: Product) => {
if (cat.id === "joints") return (p.category || '').includes("مفاصل") || p.categorySlug === 'joints';
if (cat.id === "special-care") return (p.category || '').includes("پوست") || (p.category || '').includes("مراقبت") || p.categorySlug === 'special-care';
if (cat.id === "immune") return (p.category || '').includes("ایمنی") || (p.category || '').includes("گوارش") || p.categorySlug === 'immune';
if (cat.id === "vitamins") return (p.category || '').includes("ویتامین") || (p.category || '').includes("تقویت عمومی") || (p.category || '').includes("انرژی") || p.categorySlug === 'energy' || p.categorySlug === 'general';
return (p.category || '').includes("تغذیه") || (p.category || '').includes("مکمل") || p.categorySlug === 'nutrition' || p.categorySlug === 'supplements';
};
const catProds = products.filter(p => !matchedIds.has(p.id) && matcher(p));
catProds.forEach(prod => {
matchedIds.add(prod.id);
list.push({
product: prod,
categoryMeta: cat,
@ -135,6 +146,19 @@ export default function FlipbookCatalog({ products }: FlipbookCatalogProps) {
});
});
// Catch any remaining unmatched products and add under nutrition/general
const remaining = products.filter(p => !matchedIds.has(p.id));
if (remaining.length > 0) {
const fallbackCat = categoryMetaList[categoryMetaList.length - 1] || categoryMetaList[0];
remaining.forEach(prod => {
list.push({
product: prod,
categoryMeta: fallbackCat,
pageNum: pageCounter++
});
});
}
return list;
}, [products, categoryMetaList]);
@ -301,6 +325,38 @@ export default function FlipbookCatalog({ products }: FlipbookCatalogProps) {
const prevRightPageNum = currentPage <= 3 ? 1 : currentPage - 2;
const prevLeftPageNum = currentPage <= 3 ? 0 : currentPage - 1;
// Touch swipe gesture handlers for mobile flip (RTL: swipe right -> next, swipe left -> prev)
const touchStartXRef = useRef<number | null>(null);
const touchStartYRef = useRef<number | null>(null);
const handleTouchStart = (e: React.TouchEvent) => {
if (e.touches.length === 1) {
touchStartXRef.current = e.touches[0].clientX;
touchStartYRef.current = e.touches[0].clientY;
}
};
const handleTouchEnd = (e: React.TouchEvent) => {
if (touchStartXRef.current === null || touchStartYRef.current === null) return;
if (e.changedTouches.length === 1) {
const diffX = e.changedTouches[0].clientX - touchStartXRef.current;
const diffY = e.changedTouches[0].clientY - touchStartYRef.current;
// Horizontal swipe threshold: 40px, and horizontal displacement > vertical displacement
if (Math.abs(diffX) > 40 && Math.abs(diffX) > Math.abs(diffY)) {
if (diffX > 0) {
// Swipe right in RTL -> Advance to next page
handleNext();
} else {
// Swipe left in RTL -> Go to previous page
handlePrev();
}
}
}
touchStartXRef.current = null;
touchStartYRef.current = null;
};
return (
<div
ref={containerRef}
@ -310,6 +366,8 @@ export default function FlipbookCatalog({ products }: FlipbookCatalogProps) {
: "h-[calc(100vh-140px)] min-h-[580px] max-h-[880px] py-2 px-3 sm:px-6"
}`}
dir="rtl"
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{/* ========================================================================= */}
{/* 1. TOP TOOLBAR (Controls, Search, Print, Fullscreen) - Fixed Height */}

View File

@ -13,7 +13,7 @@
"11": "MediaSelector.tsx",
"12": "index.ts",
"13": "app-audit-verification.e2e-spec.js",
"14": "UserDashboard.tsx",
"14": "CheckoutPage.tsx",
"15": "src/services/api.ts",
"16": "DoctorQueryDto",
"17": "schema.ts",
@ -21,7 +21,7 @@
"19": "RevalidationService",
"20": "CreateVideoDto",
"21": "ProductPage.tsx",
"22": "lib/services/api.ts",
"22": "toPersian",
"23": "MenuService",
"24": "BE-001",
"25": "FE-001",
@ -47,7 +47,7 @@
"45": "What You Must Do When Invoked",
"46": "HomeController",
"47": "IngredientsService",
"48": "auth.module.ts",
"48": "UsersService",
"49": "devDependencies",
"50": "devDependencies",
"51": "BlogsController",
@ -114,7 +114,7 @@
"112": "Operational Rules & Boundaries",
"113": "SmsService",
"114": "AppService",
"115": "UsersService",
"115": "AuthService",
"116": "Vazirmatn Changelog",
"117": "Vazirmatn Font فونت وزیرمتن",
"118": "Operational Rules & Boundaries",
@ -146,7 +146,7 @@
"144": "@testing-library/react",
"145": "admin.service.ts",
"146": "System Discovery",
"147": "useCartStore",
"147": "UserDashboard.tsx",
"148": "wiki/[slug]/page.tsx",
"149": "SmsSettingsPage.tsx",
"150": "Product Requirement Document (PRD)",
@ -157,7 +157,7 @@
"155": "seo-backfill.ts",
"156": "manual-test-scenarios.md",
"157": "ErrorPages.tsx",
"158": "AuthService",
"158": "@types/react-dom",
"159": "with-vpn.sh",
"160": "Architecture Specification",
"161": "Project Health Audit Report",
@ -198,7 +198,7 @@
"196": "Select.tsx",
"197": "wiki/page.tsx",
"198": "catalog/page.tsx",
"199": "RegisterDto",
"199": "auth.controller.ts",
"200": "application/README.md",
"201": "deploy.sh",
"202": "🔒 Security & Performance Review (09_devops_security)",
@ -308,6 +308,5 @@
"313": "Modal.tsx",
"315": "typescript",
"317": "revalidate/route.ts",
"318": "MaskableField.tsx",
"319": "eslint-config-next"
"318": "MaskableField.tsx"
}

File diff suppressed because one or more lines are too long

View File

@ -203,7 +203,7 @@
"201": "deploy.sh",
"202": "🔒 Security & Performance Review (09_devops_security)",
"203": "👁️ UX & Persona Interface Review (08_visual_qa)",
"204": "eslint",
"204": "eslint-config-prettier",
"205": "prisma/scientificTerms.ts",
"206": "seed-blogs.ts",
"207": "seed-custom.ts",
@ -232,7 +232,7 @@
"230": "instructions.md",
"231": "typescript",
"232": "@eslint/js",
"233": "globals",
"233": "tailwindcss",
"238": "prisma",
"239": "source-map-support",
"240": "supertest",

View File

@ -705,71 +705,6 @@
"community_name": "MediaSelector.tsx",
"norm_label": "paginationprops"
},
{
"label": "AuthorUser",
"file_type": "code",
"source_file": "frontend/admin-panel/src/pages/Blogs.tsx",
"source_location": "L54",
"_callable": true,
"_callable_class": true,
"_origin": "ast",
"id": "frontend_admin_panel_src_pages_blogs_authoruser",
"community": 11,
"community_name": "MediaSelector.tsx",
"norm_label": "authoruser"
},
{
"label": "BlogCategory",
"file_type": "code",
"source_file": "frontend/admin-panel/src/pages/Blogs.tsx",
"source_location": "L39",
"_callable": true,
"_callable_class": true,
"_origin": "ast",
"id": "frontend_admin_panel_src_pages_blogs_blogcategory",
"community": 11,
"community_name": "MediaSelector.tsx",
"norm_label": "blogcategory"
},
{
"label": "BlogPost",
"file_type": "code",
"source_file": "frontend/admin-panel/src/pages/Blogs.tsx",
"source_location": "L70",
"_callable": true,
"_callable_class": true,
"_origin": "ast",
"id": "frontend_admin_panel_src_pages_blogs_blogpost",
"community": 11,
"community_name": "MediaSelector.tsx",
"norm_label": "blogpost"
},
{
"label": "BlogTag",
"file_type": "code",
"source_file": "frontend/admin-panel/src/pages/Blogs.tsx",
"source_location": "L48",
"_callable": true,
"_callable_class": true,
"_origin": "ast",
"id": "frontend_admin_panel_src_pages_blogs_blogtag",
"community": 11,
"community_name": "MediaSelector.tsx",
"norm_label": "blogtag"
},
{
"label": "ProductSimple",
"file_type": "code",
"source_file": "frontend/admin-panel/src/pages/Blogs.tsx",
"source_location": "L62",
"_callable": true,
"_callable_class": true,
"_origin": "ast",
"id": "frontend_admin_panel_src_pages_blogs_productsimple",
"community": 11,
"community_name": "MediaSelector.tsx",
"norm_label": "productsimple"
},
{
"label": "Category",
"file_type": "code",
@ -4228,6 +4163,71 @@
"community_name": "UITexts.tsx",
"norm_label": "toggleswitchprops"
},
{
"label": "AuthorUser",
"file_type": "code",
"source_file": "frontend/admin-panel/src/pages/Blogs.tsx",
"source_location": "L54",
"_callable": true,
"_callable_class": true,
"_origin": "ast",
"id": "frontend_admin_panel_src_pages_blogs_authoruser",
"community": 55,
"community_name": "UITexts.tsx",
"norm_label": "authoruser"
},
{
"label": "BlogCategory",
"file_type": "code",
"source_file": "frontend/admin-panel/src/pages/Blogs.tsx",
"source_location": "L39",
"_callable": true,
"_callable_class": true,
"_origin": "ast",
"id": "frontend_admin_panel_src_pages_blogs_blogcategory",
"community": 55,
"community_name": "UITexts.tsx",
"norm_label": "blogcategory"
},
{
"label": "BlogPost",
"file_type": "code",
"source_file": "frontend/admin-panel/src/pages/Blogs.tsx",
"source_location": "L70",
"_callable": true,
"_callable_class": true,
"_origin": "ast",
"id": "frontend_admin_panel_src_pages_blogs_blogpost",
"community": 55,
"community_name": "UITexts.tsx",
"norm_label": "blogpost"
},
{
"label": "BlogTag",
"file_type": "code",
"source_file": "frontend/admin-panel/src/pages/Blogs.tsx",
"source_location": "L48",
"_callable": true,
"_callable_class": true,
"_origin": "ast",
"id": "frontend_admin_panel_src_pages_blogs_blogtag",
"community": 55,
"community_name": "UITexts.tsx",
"norm_label": "blogtag"
},
{
"label": "ProductSimple",
"file_type": "code",
"source_file": "frontend/admin-panel/src/pages/Blogs.tsx",
"source_location": "L62",
"_callable": true,
"_callable_class": true,
"_origin": "ast",
"id": "frontend_admin_panel_src_pages_blogs_productsimple",
"community": 55,
"community_name": "UITexts.tsx",
"norm_label": "productsimple"
},
{
"label": "AppSitePage",
"file_type": "code",
@ -6206,18 +6206,6 @@
"community_name": "MediaSelector.tsx",
"norm_label": "bannersmanager()"
},
{
"label": "Blogs()",
"file_type": "code",
"source_file": "frontend/admin-panel/src/pages/Blogs.tsx",
"source_location": "L113",
"_callable": true,
"_origin": "ast",
"id": "frontend_admin_panel_src_pages_blogs_blogs",
"community": 11,
"community_name": "MediaSelector.tsx",
"norm_label": "blogs()"
},
{
"label": "Categories()",
"file_type": "code",
@ -14126,6 +14114,18 @@
"community_name": "UITexts.tsx",
"norm_label": "toggleswitch()"
},
{
"label": "Blogs()",
"file_type": "code",
"source_file": "frontend/admin-panel/src/pages/Blogs.tsx",
"source_location": "L113",
"_callable": true,
"_origin": "ast",
"id": "frontend_admin_panel_src_pages_blogs_blogs",
"community": 55,
"community_name": "UITexts.tsx",
"norm_label": "blogs()"
},
{
"label": "createSeoSection()",
"file_type": "code",
@ -19361,17 +19361,6 @@
"community_name": "MediaSelector.tsx",
"norm_label": "bannersmanager.tsx"
},
{
"label": "Blogs.tsx",
"file_type": "code",
"source_file": "frontend/admin-panel/src/pages/Blogs.tsx",
"source_location": "L1",
"_origin": "ast",
"id": "frontend_admin_panel_src_pages_blogs",
"community": 11,
"community_name": "MediaSelector.tsx",
"norm_label": "blogs.tsx"
},
{
"label": "Categories.tsx",
"file_type": "code",
@ -19460,17 +19449,6 @@
"community_name": "MediaSelector.tsx",
"norm_label": "bannersmanager"
},
{
"label": "Blogs",
"file_type": "code",
"source_file": "frontend/admin-panel/src/routes/adminRoutes.tsx",
"source_location": "L18",
"_origin": "ast",
"id": "frontend_admin_panel_src_routes_adminroutes_blogs",
"community": 11,
"community_name": "MediaSelector.tsx",
"norm_label": "blogs"
},
{
"label": "Categories",
"file_type": "code",
@ -28558,26 +28536,26 @@
"norm_label": "\ud83d\udc41\ufe0f ux & persona interface review (08_visual_qa)"
},
{
"label": "eslint",
"label": "eslint-config-prettier",
"file_type": "code",
"source_file": "backend/package.json",
"source_location": "L63",
"source_location": "L64",
"_origin": "ast",
"id": "backend_package_devdependencies_eslint",
"id": "backend_package_devdependencies_eslint_config_prettier",
"community": 204,
"community_name": "eslint",
"norm_label": "eslint"
"community_name": "eslint-config-prettier",
"norm_label": "eslint-config-prettier"
},
{
"label": "eslint",
"label": "eslint-config-prettier",
"file_type": "concept",
"source_file": "backend/package.json",
"source_location": "L63",
"source_location": "L64",
"_origin": "ast",
"id": "backend_package_json_eslint",
"id": "eslint_config_prettier",
"community": 204,
"community_name": "eslint",
"norm_label": "eslint"
"community_name": "eslint-config-prettier",
"norm_label": "eslint-config-prettier"
},
{
"label": "prisma/scientificTerms.ts",
@ -29757,26 +29735,26 @@
"norm_label": "@eslint/js"
},
{
"label": "globals",
"label": "tailwindcss",
"file_type": "code",
"source_file": "frontend/admin-panel/package.json",
"source_location": "L35",
"source_location": "L37",
"_origin": "ast",
"id": "frontend_admin_panel_package_devdependencies_globals",
"id": "frontend_admin_panel_package_devdependencies_tailwindcss",
"community": 233,
"community_name": "globals",
"norm_label": "globals"
"community_name": "tailwindcss",
"norm_label": "tailwindcss"
},
{
"label": "globals",
"label": "tailwindcss",
"file_type": "concept",
"source_file": "frontend/admin-panel/package.json",
"source_location": "L35",
"source_location": "L37",
"_origin": "ast",
"id": "frontend_admin_panel_package_json_globals",
"id": "frontend_admin_panel_package_json_tailwindcss",
"community": 233,
"community_name": "globals",
"norm_label": "globals"
"community_name": "tailwindcss",
"norm_label": "tailwindcss"
},
{
"label": "prisma",
@ -34227,6 +34205,17 @@
"community_name": "adminRoutes.tsx",
"norm_label": "doctorsmanager"
},
{
"label": "PaymentGatewaysPage",
"file_type": "code",
"source_file": "frontend/admin-panel/src/routes/adminRoutes.tsx",
"source_location": "L35",
"_origin": "ast",
"id": "frontend_admin_panel_src_routes_adminroutes_paymentgatewayspage",
"community": 32,
"community_name": "adminRoutes.tsx",
"norm_label": "paymentgatewayspage"
},
{
"label": "Reports",
"file_type": "code",
@ -34249,17 +34238,6 @@
"community_name": "adminRoutes.tsx",
"norm_label": "router"
},
{
"label": "Settings",
"file_type": "code",
"source_file": "frontend/admin-panel/src/routes/adminRoutes.tsx",
"source_location": "L15",
"_origin": "ast",
"id": "frontend_admin_panel_src_routes_adminroutes_settings",
"community": 32,
"community_name": "adminRoutes.tsx",
"norm_label": "settings"
},
{
"label": "ShippingSettingsPage",
"file_type": "code",
@ -37164,6 +37142,17 @@
"community_name": "devDependencies",
"norm_label": "@eslint/js"
},
{
"label": "globals",
"file_type": "code",
"source_file": "frontend/admin-panel/package.json",
"source_location": "L35",
"_origin": "ast",
"id": "frontend_admin_panel_package_devdependencies_globals",
"community": 49,
"community_name": "devDependencies",
"norm_label": "globals"
},
{
"label": "postcss",
"file_type": "code",
@ -37175,17 +37164,6 @@
"community_name": "devDependencies",
"norm_label": "postcss"
},
{
"label": "tailwindcss",
"file_type": "code",
"source_file": "frontend/admin-panel/package.json",
"source_location": "L37",
"_origin": "ast",
"id": "frontend_admin_panel_package_devdependencies_tailwindcss",
"community": 49,
"community_name": "devDependencies",
"norm_label": "tailwindcss"
},
{
"label": "@types/node",
"file_type": "code",
@ -37264,15 +37242,15 @@
"norm_label": "@eslint/js"
},
{
"label": "tailwindcss",
"label": "globals",
"file_type": "concept",
"source_file": "frontend/admin-panel/package.json",
"source_location": "L37",
"source_location": "L35",
"_origin": "ast",
"id": "frontend_admin_panel_package_json_tailwindcss",
"id": "frontend_admin_panel_package_json_globals",
"community": 49,
"community_name": "devDependencies",
"norm_label": "tailwindcss"
"norm_label": "globals"
},
{
"label": "@types/node",
@ -38418,6 +38396,17 @@
"community_name": "UITexts.tsx",
"norm_label": "toggleswitch.tsx"
},
{
"label": "Blogs.tsx",
"file_type": "code",
"source_file": "frontend/admin-panel/src/pages/Blogs.tsx",
"source_location": "L1",
"_origin": "ast",
"id": "frontend_admin_panel_src_pages_blogs",
"community": 55,
"community_name": "UITexts.tsx",
"norm_label": "blogs.tsx"
},
{
"label": "UITexts.tsx",
"file_type": "code",
@ -38495,6 +38484,17 @@
"community_name": "UITexts.tsx",
"norm_label": "v1_page_tabs"
},
{
"label": "Blogs",
"file_type": "code",
"source_file": "frontend/admin-panel/src/routes/adminRoutes.tsx",
"source_location": "L18",
"_origin": "ast",
"id": "frontend_admin_panel_src_routes_adminroutes_blogs",
"community": 55,
"community_name": "UITexts.tsx",
"norm_label": "blogs"
},
{
"label": "UITexts",
"file_type": "code",
@ -41234,17 +41234,6 @@
"community_name": "Button.tsx",
"norm_label": "faqmanager"
},
{
"label": "PaymentGatewaysPage",
"file_type": "code",
"source_file": "frontend/admin-panel/src/routes/adminRoutes.tsx",
"source_location": "L35",
"_origin": "ast",
"id": "frontend_admin_panel_src_routes_adminroutes_paymentgatewayspage",
"community": 7,
"community_name": "Button.tsx",
"norm_label": "paymentgatewayspage"
},
{
"label": "Reviews",
"file_type": "code",
@ -41256,6 +41245,17 @@
"community_name": "Button.tsx",
"norm_label": "reviews"
},
{
"label": "Settings",
"file_type": "code",
"source_file": "frontend/admin-panel/src/routes/adminRoutes.tsx",
"source_location": "L15",
"_origin": "ast",
"id": "frontend_admin_panel_src_routes_adminroutes_settings",
"community": 7,
"community_name": "Button.tsx",
"norm_label": "settings"
},
{
"label": "SslSettingsPage",
"file_type": "code",
@ -44821,15 +44821,15 @@
"norm_label": "devdependencies"
},
{
"label": "eslint-config-prettier",
"label": "eslint",
"file_type": "code",
"source_file": "backend/package.json",
"source_location": "L64",
"source_location": "L63",
"_origin": "ast",
"id": "backend_package_devdependencies_eslint_config_prettier",
"id": "backend_package_devdependencies_eslint",
"community": 9,
"community_name": "devDependencies",
"norm_label": "eslint-config-prettier"
"norm_label": "eslint"
},
{
"label": "@eslint/eslintrc",
@ -44952,6 +44952,17 @@
"community_name": "devDependencies",
"norm_label": "typescript-eslint"
},
{
"label": "eslint",
"file_type": "concept",
"source_file": "backend/package.json",
"source_location": "L63",
"_origin": "ast",
"id": "backend_package_json_eslint",
"community": 9,
"community_name": "devDependencies",
"norm_label": "eslint"
},
{
"label": "globals",
"file_type": "concept",
@ -44974,17 +44985,6 @@
"community_name": "devDependencies",
"norm_label": "typescript-eslint"
},
{
"label": "eslint-config-prettier",
"file_type": "concept",
"source_file": "backend/package.json",
"source_location": "L64",
"_origin": "ast",
"id": "eslint_config_prettier",
"community": 9,
"community_name": "devDependencies",
"norm_label": "eslint-config-prettier"
},
{
"label": "@eslint/eslintrc",
"file_type": "concept",
@ -49875,7 +49875,7 @@
"confidence": "EXTRACTED",
"confidence_score": 1.0,
"source_file": "frontend/application/components/BlogPreviewSection.tsx",
"source_location": "L142",
"source_location": "L139",
"weight": 1.0,
"_origin": "ast",
"source": "frontend_application_components_blogpreviewsection_blogpreviewsection",
@ -132737,5 +132737,5 @@
}
],
"hyperedges": [],
"built_at_commit": "753b3dd7ecca4817a4473833535b2b6b765cda42"
"built_at_commit": "9371fd79e1beae3106ebc89a136ea902ef7c341d"
}

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,16 @@
# Graph Report - canina (2026-08-26)
## Corpus Check
- 586 files · ~1,330,416 words
- 586 files · ~1,330,915 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 4104 nodes · 7379 edges · 311 communities (210 shown, 101 thin omitted)
- 4104 nodes · 7379 edges · 310 communities (210 shown, 100 thin omitted)
- Extraction: 96% EXTRACTED · 4% INFERRED · 0% AMBIGUOUS · INFERRED: 281 edges (avg confidence: 0.79)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `9371fd79`
- Built from commit: `0b52f9e9`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@ -29,7 +29,7 @@
- MediaSelector.tsx
- index.ts
- app-audit-verification.e2e-spec.js
- UserDashboard.tsx
- CheckoutPage.tsx
- src/services/api.ts
- DoctorQueryDto
- schema.ts
@ -37,7 +37,7 @@
- RevalidationService
- CreateVideoDto
- ProductPage.tsx
- lib/services/api.ts
- toPersian
- MenuService
- BE-001
- FE-001
@ -63,7 +63,7 @@
- What You Must Do When Invoked
- HomeController
- IngredientsService
- auth.module.ts
- UsersService
- devDependencies
- devDependencies
- BlogsController
@ -129,7 +129,7 @@
- Operational Rules & Boundaries
- SmsService
- AppService
- UsersService
- AuthService
- Vazirmatn Changelog
- Vazirmatn Font فونت وزیرمتن
- Operational Rules & Boundaries
@ -161,7 +161,7 @@
- @testing-library/react
- admin.service.ts
- System Discovery
- useCartStore
- UserDashboard.tsx
- wiki/[slug]/page.tsx
- SmsSettingsPage.tsx
- Product Requirement Document (PRD)
@ -171,7 +171,7 @@
- Baseline Command Plan & Reconciled Command History
- seo-backfill.ts
- ErrorPages.tsx
- AuthService
- @types/react-dom
- with-vpn.sh
- Architecture Specification
- Project Health Audit Report
@ -212,7 +212,7 @@
- Select.tsx
- wiki/page.tsx
- catalog/page.tsx
- RegisterDto
- auth.controller.ts
- application/README.md
- deploy.sh
- 🔒 Security & Performance Review (09_devops_security)
@ -306,7 +306,6 @@
- typescript
- revalidate/route.ts
- MaskableField.tsx
- eslint-config-next
## God Nodes (most connected - your core abstractions)
1. `Roles()` - 106 edges
@ -337,7 +336,7 @@
- 3-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/store/cartStore.ts -> frontend/application/lib/services/api.ts`
- 4-file cycle: `frontend/application/lib/services/api.ts -> frontend/application/lib/store/userStore.ts -> frontend/application/lib/store/cartStore.ts -> frontend/application/lib/services/orderService.ts -> frontend/application/lib/services/api.ts`
## Communities (311 total, 101 thin omitted)
## Communities (310 total, 100 thin omitted)
### Community 0 - "Roles"
Cohesion: 0.24
@ -353,7 +352,7 @@ Nodes (15): SettingsController, ApiBearerAuth, ApiOkResponse, ApiOperation, ApiT
### Community 3 - "ProductService"
Cohesion: 0.06
Nodes (34): dynamic, revalidate, DEFAULT_CATEGORIES, dynamic, revalidate, dynamic, revalidate, BlogCategory (+26 more)
Nodes (35): dynamic, revalidate, DEFAULT_CATEGORIES, dynamic, revalidate, dynamic, revalidate, BlogCategory (+27 more)
### Community 4 - "SafeImage.tsx"
Cohesion: 0.09
@ -384,8 +383,8 @@ Cohesion: 0.08
Nodes (25): CreateReviewDto, ApiProperty, IsInt, IsNotEmpty, IsOptional, IsString, Min, ReviewsController (+17 more)
### Community 11 - "MediaSelector.tsx"
Cohesion: 0.08
Nodes (27): ConfirmModal(), ConfirmModalProps, getFileType(), Media, MediaSelector(), MediaSelectorProps, Pagination(), PaginationProps (+19 more)
Cohesion: 0.07
Nodes (33): ConfirmModal(), ConfirmModalProps, getFileType(), Media, MediaSelector(), MediaSelectorProps, Pagination(), PaginationProps (+25 more)
### Community 12 - "index.ts"
Cohesion: 0.06
@ -395,9 +394,9 @@ Nodes (24): backend, frontend, playwright.config.ts, @playwright/test, tests/**/
Cohesion: 0.07
Nodes (26): EnvironmentVariables, IsNotEmpty, IsOptional, IsString, validateEnv(), CustomHttpExceptionFilter, Catch, PrismaExceptionFilter (+18 more)
### Community 14 - "UserDashboard.tsx"
Cohesion: 0.11
Nodes (25): VerifyContent(), AddressModal(), AddressModalProps, BackButton(), BackButtonProps, DeleteConfirmModal(), DeleteConfirmModalProps, HeaderButton() (+17 more)
### Community 14 - "CheckoutPage.tsx"
Cohesion: 0.19
Nodes (15): AddressModal(), AddressModalProps, BackButton(), BackButtonProps, HeaderButton(), HeaderButtonProps, SearchableSelect(), SearchableSelectProps (+7 more)
### Community 15 - "src/services/api.ts"
Cohesion: 0.08
@ -427,9 +426,9 @@ Nodes (31): CreateVideoDto, ApiProperty, ApiPropertyOptional, IsBoolean, IsNotEm
Cohesion: 0.17
Nodes (12): CalculatorState, ICON_MAP, ProductPage(), ProductReviews, useCalculatorStore, ProductReviews(), ProductReviewsProps, ReviewItem (+4 more)
### Community 22 - "lib/services/api.ts"
Cohesion: 0.07
Nodes (32): AuthModal, B2BPortal, CartDrawer, ClientLayout(), LoginModal, AuthModal(), AuthModalProps, extractOtpFromText() (+24 more)
### Community 22 - "toPersian"
Cohesion: 0.10
Nodes (37): AuthModal, B2BPortal, CartDrawer, ClientLayout(), LoginModal, VerifyContent(), ArchiveProductCard(), AuthModal() (+29 more)
### Community 23 - "MenuService"
Cohesion: 0.12
@ -531,9 +530,9 @@ Nodes (10): HomeController, ApiOkResponse, ApiOperation, ApiTags, Controller, Ge
Cohesion: 0.13
Nodes (14): IngredientsController, ApiBearerAuth, ApiOperation, ApiTags, Body, Controller, Delete, Get (+6 more)
### Community 48 - "auth.module.ts"
Cohesion: 0.17
Nodes (10): DEFAULT_JWT_REFRESH_SECRET, DEFAULT_JWT_SECRET, getJwtSecret(), AuthModule, Module, JwtPayload, JwtStrategy, Injectable (+2 more)
### Community 48 - "UsersService"
Cohesion: 0.10
Nodes (12): DEFAULT_JWT_REFRESH_SECRET, DEFAULT_JWT_SECRET, getJwtSecret(), AuthModule, Module, JwtPayload, JwtStrategy, Injectable (+4 more)
### Community 49 - "devDependencies"
Cohesion: 0.11
@ -541,7 +540,7 @@ Nodes (19): autoprefixer, devDependencies, autoprefixer, eslint, @eslint/js, glo
### Community 50 - "devDependencies"
Cohesion: 0.13
Nodes (15): devDependencies, eslint, jsdom, tailwindcss, @tailwindcss/postcss, @types/node, @types/react-dom, @vitejs/plugin-react (+7 more)
Nodes (15): eslint-config-next, devDependencies, eslint, eslint-config-next, jsdom, tailwindcss, @tailwindcss/postcss, @types/node (+7 more)
### Community 51 - "BlogsController"
Cohesion: 0.07
@ -560,8 +559,8 @@ Cohesion: 0.19
Nodes (16): ApiBadRequestResponse, ApiBearerAuth, ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiTags, Body, Controller (+8 more)
### Community 55 - "UITexts.tsx"
Cohesion: 0.07
Nodes (26): ICON_REGISTRY, IconPickerModal(), IconPickerModalProps, ActiveStates, PRESET_BG_COLORS, PRESET_COLORS, RichTextEditor(), RichTextEditorProps (+18 more)
Cohesion: 0.09
Nodes (20): ICON_REGISTRY, IconPickerModal(), IconPickerModalProps, ActiveStates, PRESET_BG_COLORS, PRESET_COLORS, RichTextEditor(), RichTextEditorProps (+12 more)
### Community 56 - "Orders.tsx"
Cohesion: 0.09
@ -672,8 +671,8 @@ Cohesion: 0.12
Nodes (16): 1. Active Secret Scanning (All Modified Files), 2. Docker Container Verification (if Dockerfile exists), 3. CI/CD Basic Check (if `.github/workflows/` exists), 4. Failure Routing Protocol, 5. Forbidden Actions, ENFORCE MODE — Normal Operation, Expected JSON Output Schema, Operational Rules & Boundaries (+8 more)
### Community 85 - "ArchivePage.tsx"
Cohesion: 0.09
Nodes (19): ArchiveProductCard(), CATEGORY_MAP, ICON_MAP, BannerPlacement(), BannerPlacementProps, OrderRowSkeleton(), PetProfileSkeleton(), ProductCardSkeleton() (+11 more)
Cohesion: 0.15
Nodes (13): CATEGORY_MAP, ICON_MAP, BannerPlacement(), BannerPlacementProps, PetType, B2BInquiry, Banner, DosageConfig (+5 more)
### Community 86 - "zibal.service.ts"
Cohesion: 0.16
@ -689,7 +688,7 @@ Nodes (12): prisma, main(), prisma, determineSuitableFor(), findOrCreateCategory
### Community 90 - "useSettingsStore"
Cohesion: 0.09
Nodes (27): HomeClient(), HomeClientProps, B2BLandingClient(), BlogPost, BlogPreviewSection(), BrandLogo(), BrandLogoProps, ContactInfoItem (+19 more)
Nodes (29): HomeClient(), HomeClientProps, B2BLandingClient(), BlogPost, BlogPreviewSection(), BrandLogo(), BrandLogoProps, ContactInfoItem (+21 more)
### Community 91 - "Reconciled Audit Roles & Assignments"
Cohesion: 0.12
@ -779,6 +778,10 @@ Nodes (10): 1. Pre-Deployment Checklist, 2. Build Verification, 3. Deployment St
Cohesion: 0.29
Nodes (5): AppController, Controller, Get, AppService, Injectable
### Community 115 - "AuthService"
Cohesion: 0.14
Nodes (4): ApiErr, AuthResponse, AuthService, User
### Community 116 - "Vazirmatn Changelog"
Cohesion: 0.18
Nodes (10): 32.0.0, 32.1, 32.101, 32.102, 33.000, 33.001, 33.002, 33.003 (+2 more)
@ -899,9 +902,9 @@ Nodes (13): CouponInput, CouponTargetInput, PaginationQuery, CreateUserDto, ApiP
Cohesion: 0.25
Nodes (7): Active Core Applications, Current Architecture, Evidence-Based Status Matrix, Excluded / Non-Auditable Artifacts, Major Business Domains Discovered, System Discovery, Technical Architecture Summary
### Community 147 - "useCartStore"
Cohesion: 0.18
Nodes (13): CheckoutPage(), ProductCard(), OrderSuccess(), OrderTracking(), PetProfile(), SearchResultsPage(), useCartStore, HealthLog (+5 more)
### Community 147 - "UserDashboard.tsx"
Cohesion: 0.10
Nodes (17): DeleteConfirmModal(), DeleteConfirmModalProps, OrderDetailsModal(), OrderRowSkeleton(), PetProfileSkeleton(), ProductCardSkeleton(), Skeleton(), SkeletonProps (+9 more)
### Community 148 - "wiki/[slug]/page.tsx"
Cohesion: 0.60
@ -916,8 +919,8 @@ Cohesion: 0.29
Nodes (6): 1. Executive Vision, 2. Target Audience, 3. Functional Requirements, 4. Non-Functional Requirements (Performance, Security), 5. Epic / Feature Breakdown, Product Requirement Document (PRD)
### Community 151 - "auth.service.ts"
Cohesion: 0.08
Nodes (25): AdminLoginInput, LoginInput, RegisterInput, AdminLoginDto, ApiProperty, IsEmail, IsNotEmpty, IsString (+17 more)
Cohesion: 0.09
Nodes (17): AdminLoginInput, AuthService, LoginInput, RegisterInput, Injectable, SendOtpDto, ApiProperty, IsNotEmpty (+9 more)
### Community 152 - "cartStore.ts"
Cohesion: 0.15
@ -935,10 +938,6 @@ Nodes (6): Attempted Command Execution Log, Backend `backend/package.json` Scrip
Cohesion: 0.67
Nodes (3): prisma, runSeoBackfill(), stripHtml()
### Community 158 - "AuthService"
Cohesion: 0.19
Nodes (3): AuthService, Injectable, normalizeMobile()
### Community 159 - "with-vpn.sh"
Cohesion: 0.62
Nodes (6): cleanup(), log(), with-vpn.sh script, start_vpn(), stop_vpn(), vpn_is_up()
@ -1071,9 +1070,9 @@ Nodes (3): Select, SelectOption, SelectProps
Cohesion: 0.40
Nodes (3): generateMetadata(), formatIngredientDisplayName(), IngredientWiki()
### Community 199 - "RegisterDto"
Cohesion: 0.22
Nodes (8): RegisterDto, ApiProperty, ApiPropertyOptional, IsEmail, IsNotEmpty, IsOptional, IsString, MinLength
### Community 199 - "auth.controller.ts"
Cohesion: 0.10
Nodes (19): AdminLoginDto, ApiProperty, IsEmail, IsNotEmpty, IsString, MinLength, LoginDto, ApiProperty (+11 more)
### Community 200 - "application/README.md"
Cohesion: 0.50
@ -1106,7 +1105,7 @@ Nodes (3): GET(), handleRevalidate(), POST()
## Knowledge Gaps
- **1335 isolated node(s):** `$schema`, `collection`, `sourceRoot`, `deleteOutDir`, `name` (+1330 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **101 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
- **100 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
@ -1115,13 +1114,13 @@ _Questions this graph is uniquely positioned to answer:_
_High betweenness centrality (0.086) - this node is a cross-community bridge._
- **Why does `Roles()` connect `Roles` to `WholesaleApplyDto`, `B2BService`, `SettingsController`, `FaqService`, `CmsController`, `tickets.controller.ts`, `SslController`, `BannersService`, `CreateReviewDto`, `TestimonialsService`, `IngredientsService`, `JwtAuthGuard`, `RevalidationService`, `PrescriptionsService`, `SmartAdvisorService`, `MenuService`, `ContactService`?**
_High betweenness centrality (0.067) - this node is a cross-community bridge._
- **Why does `JwtAuthGuard` connect `JwtAuthGuard` to `PetsController`, `CmsController`, `tickets.controller.ts`, `admin.module.ts`, `PetsController`, `DoctorQueryDto`, `admin.service.ts`, `PaginationDto`, `RevalidationService`, `CreateVideoDto`, `auth.service.ts`, `users.controller.ts`?**
- **Why does `JwtAuthGuard` connect `JwtAuthGuard` to `PetsController`, `CmsController`, `tickets.controller.ts`, `auth.controller.ts`, `admin.module.ts`, `PetsController`, `DoctorQueryDto`, `admin.service.ts`, `PaginationDto`, `RevalidationService`, `CreateVideoDto`, `users.controller.ts`?**
_High betweenness centrality (0.034) - this node is a cross-community bridge._
- **What connects `$schema`, `collection`, `sourceRoot` to the rest of the system?**
_1335 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `app.module.ts` be split into smaller, more focused modules?**
_Cohesion score 0.06988120195667366 - nodes in this community are weakly interconnected._
- **Should `ProductService` be split into smaller, more focused modules?**
_Cohesion score 0.06187202538339503 - nodes in this community are weakly interconnected._
_Cohesion score 0.060515873015873016 - nodes in this community are weakly interconnected._
- **Should `SafeImage.tsx` be split into smaller, more focused modules?**
_Cohesion score 0.09462365591397849 - nodes in this community are weakly interconnected._

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