feat(catalog): redesign digital catalog spread with luxury European apothecary layout and mobile gesture navigation
Some checks failed
Deploy Canina / deploy (push) Successful in 2m1s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 0s

This commit is contained in:
parsa aghaei 2026-09-05 17:16:35 +03:30
parent 30fb8f3e83
commit 00cfd1a2a0
2 changed files with 314 additions and 201 deletions

View File

@ -26,9 +26,11 @@ import {
export interface CategoryMeta {
id: string;
name: string;
color: string;
lightBg: string;
borderBg: string;
color: string; // پایه تیره
lightBg: string; // پنل روشن
borderBg: string; // اکسنت
accentColor?: string;
secondaryColor?: string;
icon: React.ElementType;
tagline: string;
startPage: number;
@ -201,38 +203,41 @@ export default function CatalogPageSpread({
// ==========================================
if (pageType === "toc") {
return (
<div className="w-full h-full bg-slate-50 text-slate-800 p-5 md:p-7 flex flex-col justify-between select-none box-border" dir="rtl">
<div className="w-full h-full bg-[#FAF8F5] text-slate-900 p-5 md:p-7 flex flex-col justify-between select-none box-border" dir="rtl" style={{ fontFamily: "'Vazirmatn', sans-serif" }}>
{/* TOC Header */}
<div className="border-b border-slate-200 pb-3 flex items-center justify-between shrink-0">
<div className="border-b pb-3 flex items-center justify-between shrink-0" style={{ borderColor: "rgba(18,33,31,0.1)" }}>
<div>
<div className="flex items-center gap-1.5 text-[10px] font-black text-canina-blue uppercase tracking-widest">
<div className="flex items-center gap-1.5 text-[9.5px] font-black uppercase tracking-widest text-[#C88A3F]">
<BookOpen className="w-3.5 h-3.5" />
<span>فهرست تخصصی و راهنمای بالینی</span>
</div>
<h2 className="text-xl md:text-2xl font-black font-lalezar text-slate-900 mt-0.5">
<h2 className="text-xl md:text-2xl font-black text-slate-900 mt-0.5 tracking-tight font-vazir">
دستهبندیهای کاتالوگ دارویی
</h2>
</div>
<div className="text-left">
<span className="text-[10px] font-black text-slate-400 block font-mono">CANINA PHARMA</span>
<span className="text-[9px] font-bold text-amber-600 uppercase">Germany</span>
<span className="text-[10px] font-medium text-slate-400 block font-mono tracking-wider">CANINA PHARMA</span>
<span className="text-[9px] font-medium text-[#C88A3F] uppercase">Germany</span>
</div>
</div>
{/* Categories List (Compact luxury layout, perfectly fitted) */}
<div className="my-auto space-y-2.5 py-2 flex-1 flex flex-col justify-center min-h-0">
<div className="my-auto space-y-2 py-2 flex-1 flex flex-col justify-center min-h-0">
{categoriesList.map((cat, idx) => {
const IconComp = cat.icon;
const accent = cat.accentColor || cat.borderBg || "#C88A3F";
const sec = cat.secondaryColor || "#6B8F71";
return (
<div
key={cat.id}
onClick={() => onJumpToPage && onJumpToPage(cat.startPage)}
className="bg-white border border-slate-200/90 hover:border-slate-300 rounded-xl p-2.5 sm:p-3 flex items-center justify-between gap-3 shadow-2xs hover:shadow-md transition-all cursor-pointer group"
className="bg-white border rounded-xl p-2.5 sm:p-3 flex items-center justify-between gap-3 shadow-2xs hover:shadow-md transition-all cursor-pointer group"
style={{ borderColor: "rgba(18,33,31,0.08)" }}
>
<div className="flex items-center gap-3 min-w-0">
{/* Category Color Icon */}
<div
className="w-9 h-9 rounded-lg flex items-center justify-center text-white font-black shadow-xs shrink-0 group-hover:scale-105 transition-transform"
className="w-9 h-9 rounded-lg flex items-center justify-center text-white font-medium shadow-xs shrink-0 group-hover:scale-105 transition-transform"
style={{ backgroundColor: cat.color }}
>
<IconComp className="w-4 h-4" />
@ -240,15 +245,15 @@ export default function CatalogPageSpread({
<div className="min-w-0">
<div className="flex items-center gap-1.5">
<span className="text-[9px] font-black text-slate-400 font-mono">
<span className="text-[8.5px] font-medium font-mono" style={{ color: sec }}>
SECTION 0{idx + 1}
</span>
<span className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: cat.color }} />
<span className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: accent }} />
</div>
<h3 className="text-xs sm:text-sm font-black text-slate-900 group-hover:text-canina-blue transition-colors font-vazir truncate">
<h3 className="text-xs sm:text-sm font-black text-slate-900 group-hover:text-[#C88A3F] transition-colors font-vazir truncate">
{cat.name}
</h3>
<p className="text-[10px] text-slate-500 font-medium truncate mt-0.5">
<p className="text-[9.5px] text-slate-500 font-normal truncate mt-0.5">
{cat.tagline}
</p>
</div>
@ -256,12 +261,12 @@ export default function CatalogPageSpread({
<div className="flex items-center gap-2 shrink-0">
<div className="text-left">
<span className="text-[8px] text-slate-400 font-bold block">صفحه</span>
<span className="text-[8px] text-slate-400 font-normal block">صفحه</span>
<span className="text-xs font-black text-slate-900 font-mono">
{cat.startPage < 10 ? `۰${cat.startPage}` : cat.startPage}
</span>
</div>
<div className="w-6 h-6 rounded-full bg-slate-100 group-hover:bg-canina-blue group-hover:text-white text-slate-400 flex items-center justify-center transition-colors">
<div className="w-6 h-6 rounded-full bg-slate-100 group-hover:bg-slate-900 group-hover:text-white text-slate-400 flex items-center justify-center transition-colors">
<ChevronLeft className="w-3.5 h-3.5" />
</div>
</div>
@ -271,17 +276,17 @@ export default function CatalogPageSpread({
</div>
{/* Quality Seal Note */}
<div className="bg-amber-50/70 border border-amber-200/60 rounded-xl p-2.5 flex items-center gap-2.5 shrink-0">
<Award className="w-4 h-4 text-amber-600 shrink-0" />
<p className="text-[10px] text-amber-900 font-bold leading-relaxed">
<div className="bg-[#FAF0DE]/60 border border-[#D97D3D]/20 rounded-xl p-2.5 flex items-center gap-2.5 shrink-0">
<Award className="w-4 h-4 text-[#D97D3D] shrink-0" />
<p className="text-[9.5px] text-[#2B1810] font-medium leading-relaxed">
تمامی فرآوردههای کنینا آلمان مطابق با مونوگرافهای دارویی فارماکوپه اروپا (Ph. Eur.) و تحت استاندارد GMP تولید میشوند.
</p>
</div>
{/* TOC Footer */}
<div className="border-t border-slate-200 pt-2 flex items-center justify-between text-[10px] text-slate-400 font-bold shrink-0">
<div className="border-t pt-2 flex items-center justify-between text-[9.5px] text-slate-400 font-normal shrink-0" style={{ borderColor: "rgba(18,33,31,0.08)" }}>
<span>راهنمای جامع کاتالوگ دارویی Canina Pharma</span>
<span className="font-mono">صفحه ۰۲</span>
<span className="font-mono text-[#C88A3F]">صفحه ۰۲</span>
</div>
</div>
);
@ -349,205 +354,258 @@ export default function CatalogPageSpread({
}
// ==========================================
// 4. DEDICATED 1-PAGE LUXURY PRODUCT PRESENTATION
// (صفحه اختصاصی و لوکس هر محصول)
// 4. DEDICATED LUXURY PHARMACEUTICAL MONOGRAPH PRODUCT PRESENTATION
// (صفحه اختصاصی، لوکس و مونوگراف کاتالوگ دارویی اروپایی)
// ==========================================
if (!product) return null;
const formBadge = getFormBadge(product.unit, product.name);
const speciesBadge = getSpeciesBadge(product.suitableFor);
const FormIcon = formBadge.icon;
const SpeciesIcon = speciesBadge.icon;
const themeColor = categoryMeta?.color || "#0284C7";
// Locked 5-category European luxury palette
const baseDark = categoryMeta?.color || "#12211F";
const panelLight = categoryMeta?.lightBg || "#F3EEE0";
const accentColor = categoryMeta?.accentColor || categoryMeta?.borderBg || "#C88A3F";
const secondaryColor = categoryMeta?.secondaryColor || "#6B8F71";
const CategoryIcon = categoryMeta?.icon || Activity;
// Alternating layout dynamics based on page number (odd vs even pages)
const isEvenPage = pageNumber % 2 === 0;
// Clean description without HTML
const cleanDescription = (product.shortDescription || (product.description ? product.description.replace(/<[^>]*>/g, '') : '')).trim();
// Parse dosage into sequential steps if available
const dosageRaw = (product.dosage_logic || product.feedingAdvice || "").trim();
const dosageSteps = dosageRaw
? dosageRaw
.split(/(?:؛|\. |\n| - )/g)
.map(s => s.trim())
.filter(s => s.length > 5)
.slice(0, 3)
: [];
return (
<div className="w-full h-full bg-gradient-to-b from-slate-50 via-white to-slate-50 text-slate-800 p-4 sm:p-5 md:p-6 flex flex-col justify-between select-none box-border overflow-hidden relative" dir="rtl">
{/* Decorative luxury gradient tint border on top edge */}
<div
className="w-full h-full flex flex-col justify-between select-none box-border overflow-hidden relative"
style={{
backgroundColor: panelLight,
color: "#18201E",
fontFamily: "'Vazirmatn', sans-serif",
direction: "rtl"
}}
>
{/* Category Delicate Line Watermark in Background */}
<div
className="absolute top-0 right-0 left-0 h-1.5 opacity-80"
style={{ backgroundColor: themeColor }}
className="absolute -bottom-14 -left-14 w-64 h-64 pointer-events-none opacity-[0.04] transition-all"
style={{ color: baseDark }}
>
<CategoryIcon className="w-full h-full" strokeWidth={1} />
</div>
{/* Subtle Luxury Gradient Ambient Aura */}
<div
className="absolute top-0 right-0 w-72 h-72 rounded-full pointer-events-none blur-3xl opacity-[0.08]"
style={{ background: `radial-gradient(circle, ${accentColor} 0%, transparent 70%)` }}
/>
{/* 1. Header Bar: Category, Species, Form, ArtNo */}
<div className="border-b border-slate-200/80 pb-2 flex items-center justify-between gap-2 shrink-0 pt-0.5">
{/* Category Pill */}
<div className="flex items-center gap-1.5 min-w-0">
<span
className="w-2.5 h-2.5 rounded-full shrink-0 shadow-2xs"
style={{ backgroundColor: themeColor }}
/>
<span className="text-[10px] sm:text-[11px] font-black uppercase tracking-wider text-slate-800 font-vazir truncate whitespace-nowrap">
{product.category}
</span>
{/* Top Accent Rim (Ultra-thin luxury rule) */}
<div
className="h-1 w-full shrink-0"
style={{ backgroundColor: accentColor }}
/>
{/* 1. Header Bar: Minimal Monograph Classification */}
<div className="px-5 pt-3 pb-2 flex items-center justify-between border-b shrink-0" style={{ borderColor: "rgba(0,0,0,0.06)" }}>
<div className="flex items-center gap-2.5">
<div
className="w-6 h-6 rounded-full flex items-center justify-center shrink-0 border"
style={{
borderColor: `${accentColor}55`,
backgroundColor: `${accentColor}12`,
color: accentColor
}}
>
<CategoryIcon className="w-3.5 h-3.5" />
</div>
<div>
<span className="text-[10px] font-medium block leading-none" style={{ color: secondaryColor }}>
{product.category}
</span>
<span className="text-[8px] font-mono tracking-widest opacity-40 block mt-0.5" style={{ color: baseDark }}>
CANINA VETERINARY DOSSIER
</span>
</div>
</div>
{/* Product Badges (Strictly whitespace-nowrap) */}
<div className="flex items-center gap-1 sm:gap-1.5 text-[9px] font-bold shrink-0">
<span className="bg-white text-slate-700 px-2 py-0.5 rounded-md flex items-center gap-1 border border-slate-200 shadow-3xs whitespace-nowrap">
{speciesBadge.isBoth ? (
<span className="flex items-center gap-0.5">
<Dog className="w-3 h-3 text-slate-500 shrink-0" />
<Cat className="w-3 h-3 text-slate-500 shrink-0" />
</span>
) : (
<SpeciesIcon className="w-3 h-3 text-slate-500 shrink-0" />
)}
<span className="whitespace-nowrap">{speciesBadge.label}</span>
</span>
<span className="bg-white text-slate-700 px-2 py-0.5 rounded-md flex items-center gap-1 border border-slate-200 shadow-3xs whitespace-nowrap">
<FormIcon className="w-3 h-3 text-slate-500 shrink-0" />
<span className="whitespace-nowrap">{formBadge.label}</span>
</span>
<span className="bg-blue-50/90 text-canina-blue font-mono px-2 py-0.5 rounded-md border border-blue-100/90 whitespace-nowrap font-bold">
<div className="flex items-center gap-2 text-left">
<span
className="text-[9px] font-mono font-medium px-2 py-0.5 rounded-full border"
style={{
borderColor: `${accentColor}40`,
color: accentColor,
backgroundColor: `${accentColor}08`
}}
>
Art-Nr: {product.artNo}
</span>
<span className="text-[9px] font-medium opacity-60 hidden sm:inline" style={{ color: baseDark }}>
{speciesBadge.label}
</span>
</div>
</div>
{/* 2. Main Body Content (Luxury grid fitted strictly without overflow) */}
<div className="flex-1 min-h-0 flex flex-col justify-between py-1.5 space-y-1.5">
{/* 2. Main Body Content: Asymmetric editorial composition */}
<div className="flex-1 min-h-0 px-5 py-2 flex flex-col justify-between">
{/* Top Product Hero Showcase (Image + Titles + Special Badge) */}
<div className="flex items-center gap-3 bg-white border border-slate-200/90 rounded-2xl p-2.5 shrink-0 shadow-2xs relative overflow-hidden">
<div
className="absolute -top-8 -right-8 w-24 h-24 rounded-full opacity-10 blur-xl pointer-events-none"
style={{ backgroundColor: themeColor }}
/>
{/* Hero Product Stage: Studio Lighting Mockup + Three-Tier Typography */}
<div className={`flex items-center gap-4 py-1 shrink-0 ${isEvenPage ? 'flex-row' : 'flex-row-reverse'}`}>
{/* Studio Image Box */}
<div className="w-20 h-20 sm:w-24 sm:h-24 bg-slate-50/80 rounded-xl border border-slate-200/70 p-1.5 flex items-center justify-center shrink-0 relative shadow-inner">
<SafeImage
src={product.image}
alt={product.name}
className="w-full h-full"
imgClassName="object-contain max-h-16 sm:max-h-20 filter drop-shadow-md"
{/* Studio Packshot with Realistic Lighting & Depth */}
<div className="relative w-28 h-28 sm:w-32 sm:h-32 shrink-0 flex items-center justify-center">
{/* Ambient Studio Spotlight Aura */}
<div
className="absolute inset-0 rounded-full blur-xl pointer-events-none opacity-20"
style={{ background: `radial-gradient(circle, ${accentColor} 0%, transparent 70%)` }}
/>
{/* Thin Aesthetic Ring behind product */}
<div
className="absolute w-24 h-24 sm:w-28 sm:h-28 rounded-full border border-dashed pointer-events-none opacity-30"
style={{ borderColor: accentColor }}
/>
<div className="relative z-10 w-full h-full flex items-center justify-center">
<SafeImage
src={product.image}
alt={product.name}
className="w-full h-full"
imgClassName="object-contain max-h-24 sm:max-h-28 filter drop-shadow-[0_12px_18px_rgba(0,0,0,0.22)]"
/>
</div>
{product.unit && (
<span className="absolute bottom-1 right-1 bg-slate-900/90 text-white font-mono text-[7px] sm:text-[8px] font-bold px-1.5 py-0.2 rounded shadow-2xs whitespace-nowrap">
<span
className="absolute bottom-0 right-1 text-[8px] font-mono font-medium px-1.5 py-0.5 rounded shadow-xs"
style={{ backgroundColor: baseDark, color: panelLight }}
>
{product.unit}
</span>
)}
</div>
{/* Title & Scientific Tagline */}
<div className="min-w-0 flex-1 space-y-0.5">
{/* Three-Tier Typography: Black Name, Advertising Subtitle, Editorial Body */}
<div className="min-w-0 flex-1 space-y-1">
{product.specialBadge && (
<span
className="text-[8px] font-black px-2 py-0.5 rounded-full text-white inline-block shadow-2xs whitespace-nowrap mb-0.5"
style={{ backgroundColor: themeColor }}
className="text-[8px] font-medium tracking-wide inline-block px-2 py-0.5 rounded-full mb-0.5 border"
style={{
color: baseDark,
borderColor: `${accentColor}60`,
backgroundColor: `${accentColor}18`
}}
>
{product.specialBadge}
{product.specialBadge}
</span>
)}
<h2 className="text-sm sm:text-base font-black text-slate-900 font-vazir leading-tight truncate">
<h2
className="text-base sm:text-lg font-black leading-tight truncate font-vazir tracking-tight"
style={{ color: baseDark }}
>
{product.name}
</h2>
{product.scientificTagline && (
<p className="text-[9px] sm:text-[10px] text-slate-600 font-bold italic truncate">
{product.scientificTagline}
</p>
)}
<p className="text-[9px] text-slate-500 font-medium line-clamp-2 leading-relaxed text-justify">
{product.shortDescription || (product.description ? product.description.replace(/<[^>]*>/g, '') : '')}
<p
className="text-[10px] sm:text-[11px] font-medium leading-snug line-clamp-2"
style={{ color: accentColor }}
>
{product.scientificTagline || "فرمولاسیون دارویی استاندارد جهت ارتقای سلامت زیستی و بالینی پت"}
</p>
<p
className="text-[9px] font-normal leading-relaxed opacity-75 line-clamp-2 text-justify"
style={{ color: baseDark }}
>
{cleanDescription}
</p>
</div>
</div>
{/* Clinical Indications & Symptoms */}
{/* Section 1: Clinical Indications (Flowing text with refined dot separators, no colored badges) */}
{product.symptoms && product.symptoms.length > 0 && (
<div className="space-y-1 shrink-0">
<span className="text-[8px] font-black text-slate-400 uppercase tracking-widest block whitespace-nowrap">
موارد مصرف و نشانههای بالینی:
<div className="pt-1 pb-0.5 shrink-0 border-t" style={{ borderColor: "rgba(0,0,0,0.06)" }}>
<span className="text-[9px] font-black block mb-0.5" style={{ color: accentColor }}>
موارد مصرف بالینی
</span>
<div className="flex flex-wrap gap-1">
{product.symptoms.slice(0, 4).map((symptom, i) => (
<span
key={i}
className="bg-red-50 text-red-900 border border-red-200/80 rounded-md px-1.5 py-0.5 text-[8.5px] font-bold whitespace-nowrap shadow-3xs"
>
{symptom}
</span>
<p className="text-[9px] font-normal leading-relaxed text-justify m-0" style={{ color: "#2B3633" }}>
{product.symptoms.map((symptom, i) => (
<React.Fragment key={i}>
{i > 0 && (
<span
className="inline-block w-1.5 h-1.5 rounded-full mx-1.5 align-middle"
style={{ backgroundColor: accentColor }}
/>
)}
<span>{symptom}</span>
</React.Fragment>
))}
</div>
</div>
)}
{/* Active Ingredients & Composition Table */}
{product.main_ingredients && product.main_ingredients.length > 0 && (
<div className="bg-slate-50/90 border border-slate-200/80 rounded-xl p-2 space-y-1 shrink-0">
<div className="flex items-center gap-1 text-[8.5px] font-black text-canina-blue uppercase whitespace-nowrap">
<Layers className="w-3 h-3 shrink-0" />
<span>ترکیبات موثره و فرمولاسیون دارویی:</span>
</div>
<div className="flex flex-wrap gap-1">
{product.main_ingredients.slice(0, 4).map((ing, idx) => (
<span
key={idx}
className="bg-white text-slate-800 border border-slate-200 rounded px-1.5 py-0.5 text-[8.5px] font-bold shadow-3xs whitespace-nowrap"
>
{ing}
</span>
))}
</div>
</div>
)}
{/* Clinical Dosage Protocol */}
{product.dosage_logic && (
<div className="bg-amber-50/80 border border-amber-200/80 rounded-xl p-2 space-y-0.5 shrink-0">
<div className="flex items-center gap-1 text-amber-900 font-black text-[8.5px] whitespace-nowrap">
<ShieldCheck className="w-3 h-3 text-amber-600 shrink-0" />
<span>پروتکل بالینی تجویز و دوز مصرف:</span>
</div>
<p className="text-[8.5px] text-amber-950 font-bold leading-relaxed line-clamp-2">
{product.dosage_logic}
</p>
</div>
)}
{/* Specialist Endorsement */}
{product.specialist && product.specialist.name && (
<div className="bg-white border border-slate-200/80 rounded-xl p-2 flex items-center gap-2 shrink-0 shadow-3xs">
<div className="w-7 h-7 rounded-full bg-canina-blue/10 border border-canina-blue/20 text-canina-blue flex items-center justify-center font-black text-xs font-mono shadow-3xs shrink-0 overflow-hidden">
{product.specialist.image ? (
<img
src={product.specialist.image}
alt={product.specialist.name}
className="w-full h-full object-cover"
onError={(e) => {
(e.target as HTMLElement).style.display = 'none';
}}
/>
) : (
<Stethoscope className="w-3.5 h-3.5" />
)}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1 truncate">
<span className="text-[8.5px] font-black text-slate-900 truncate">{product.specialist.name}</span>
{product.specialist.title && (
<span className="text-[7.5px] text-slate-500 truncate font-medium">({product.specialist.title})</span>
)}
{/* Section 2: Active Formulation (Short elegant paragraph, no tables) */}
{product.main_ingredients && product.main_ingredients.length > 0 && (
<div className="pt-1 pb-0.5 shrink-0 border-t" style={{ borderColor: "rgba(0,0,0,0.06)" }}>
<span className="text-[9px] font-black block mb-0.5" style={{ color: accentColor }}>
ترکیبات و فرمولاسیون
</span>
<p className="text-[9px] font-normal leading-relaxed text-justify m-0" style={{ color: "#2B3633" }}>
{product.main_ingredients.join("، ")}.
</p>
</div>
)}
{/* Section 3: Clinical Dosage Protocol (Refined sequential numbering ۰۱, ۰۲ or single note) */}
{dosageRaw && (
<div className="pt-1 pb-1 shrink-0 border-t" style={{ borderColor: "rgba(0,0,0,0.06)" }}>
<span className="text-[9px] font-black block mb-1" style={{ color: accentColor }}>
پروتکل دوز و نحوه مصرف
</span>
{dosageSteps.length > 1 ? (
<div className="space-y-0.5">
{dosageSteps.map((step, idx) => {
const numStr = idx === 0 ? "۰۱" : idx === 1 ? "۰۲" : "۰۳";
return (
<div key={idx} className="flex items-baseline gap-1.5 text-[8.5px] leading-relaxed">
<span className="font-mono font-bold shrink-0 text-[9px]" style={{ color: secondaryColor }}>
{numStr}
</span>
<span className="font-normal opacity-90" style={{ color: "#2B3633" }}>
{step}
</span>
</div>
);
})}
</div>
{product.specialist.message && (
<p className="text-[8px] text-slate-600 italic truncate mt-0.2">
«{product.specialist.message}»
</p>
)}
</div>
) : (
<p className="text-[8.5px] font-normal leading-relaxed text-justify m-0" style={{ color: "#2B3633" }}>
{dosageRaw}
</p>
)}
</div>
)}
</div>
{/* 3. Page Footer */}
<div className="border-t border-slate-200/80 pt-1.5 flex items-center justify-between text-[8.5px] text-slate-400 font-bold shrink-0">
<div className="flex items-center gap-1.5 whitespace-nowrap">
<Award className="w-3 h-3 text-amber-500 shrink-0" />
<span>Canina Pharma Germany استانداردهای بالینی GMP</span>
</div>
<span className="font-mono text-slate-600 bg-slate-100 px-2 py-0.2 rounded-md whitespace-nowrap font-bold">
{/* 3. Page Footer: Minimal luxury accreditation & page number */}
<div
className="px-5 py-2 border-t flex items-center justify-between text-[8.5px] font-normal shrink-0"
style={{ borderColor: "rgba(0,0,0,0.06)", color: "rgba(24, 32, 30, 0.6)" }}
>
<span>
Canina Pharma Germany Ph. Eur. & GMP Certified
</span>
<span className="font-mono font-medium" style={{ color: accentColor }}>
صفحه {pageNumber < 10 ? `۰${pageNumber}` : pageNumber} از {totalPages}
</span>
</div>

View File

@ -46,35 +46,52 @@ export default function FlipbookCatalog({ products }: FlipbookCatalogProps) {
const containerRef = useRef<HTMLDivElement>(null);
// 1. Build Category Metadata with dynamic starting pages
// 1. Build Category Metadata with dynamic starting pages and luxury European apothecary palette
const categoryMetaList: CategoryMeta[] = useMemo(() => {
const cats: { id: string; name: string; color: string; lightBg: string; borderBg: string; icon: React.ElementType; tagline: string; matcher: (p: Product) => boolean }[] = [
const cats: {
id: string;
name: string;
color: string;
lightBg: string;
borderBg: string;
accentColor?: string;
secondaryColor?: string;
icon: React.ElementType;
tagline: string;
matcher: (p: Product) => boolean
}[] = [
{
id: "joints",
name: "مفاصل، استخوان و اسکلت",
color: "#0284C7",
lightBg: "bg-sky-50",
borderBg: "border-sky-200",
name: "مفاصل و استخوان",
color: "#12211F", // پایه پاین تیره
lightBg: "#F3EEE0", // پنل روشن استخوانی
borderBg: "#C88A3F", // اکسنت کهربا / برنز
accentColor: "#C88A3F",
secondaryColor: "#6B8F71", // خزه‌ای کمرنگ
icon: Activity,
tagline: "بازسازی غضروف، هیدروکسی آپاتیت طبیعی و تراکم استخوان",
matcher: (p: Product) => (p.category || '').includes("مفاصل") || p.categorySlug === 'joints'
matcher: (p: Product) => (p.category || '').includes("مفاصل") || (p.category || '').includes("استخوان") || p.categorySlug === 'joints'
},
{
id: "special-care",
name: "پوست، مو و بهداشت تخصصی",
color: "#0D9488",
lightBg: "bg-teal-50",
borderBg: "border-teal-200",
color: "#1A1F2E", // نیلی تیره
lightBg: "#EFEDF5", // خاکستری مرواریدی
borderBg: "#8C7BC9", // بنفش دوداری
accentColor: "#8C7BC9",
secondaryColor: "#B79FE0", // یاسی روشن
icon: Sparkles,
tagline: "تکنولوژی نقره میکروسیلور، اسیدهای چرب امگا و ترمیم بیولوژیک",
matcher: (p: Product) => (p.category || '').includes("پوست") || (p.category || '').includes("مراقبت") || p.categorySlug === 'special-care'
matcher: (p: Product) => (p.category || '').includes("پوست") || (p.category || '').includes("مراقبت") || (p.category || '').includes("بهداشت") || p.categorySlug === 'special-care'
},
{
id: "immune",
name: "سیستم ایمنی و گوارش",
color: "#16A34A",
lightBg: "bg-emerald-50",
borderBg: "border-emerald-200",
color: "#14241F", // سبز جنگلی تیره
lightBg: "#EEF2E8", // سبز شیری
borderBg: "#7FA05C", // سبز زیتونی
accentColor: "#7FA05C",
secondaryColor: "#C9A15A", // طلایی خاکرس
icon: ShieldCheck,
tagline: "خمیر آغوز متمرکز (Colostrum) و تقویت فوری ایمونوگلوبولین‌ها",
matcher: (p: Product) => (p.category || '').includes("ایمنی") || (p.category || '').includes("گوارش") || p.categorySlug === 'immune'
@ -82,9 +99,11 @@ export default function FlipbookCatalog({ products }: FlipbookCatalogProps) {
{
id: "vitamins",
name: "ویتامین‌ها و انرژی‌بخش‌ها",
color: "#D97706",
lightBg: "bg-amber-50",
borderBg: "border-amber-200",
color: "#2B1810", // قهوه‌ای سوخته تیره
lightBg: "#FBF0DE", // کرم گرم
borderBg: "#D97D3D", // نارنجی زنگاری
accentColor: "#D97D3D",
secondaryColor: "#E8A94B", // کهربای روشن
icon: Zap,
tagline: "تامین عناصر کمیاب، هموگلوبین و توان فیزیکی پت‌ها",
matcher: (p: Product) => (p.category || '').includes("ویتامین") || (p.category || '').includes("تقویت عمومی") || (p.category || '').includes("انرژی") || p.categorySlug === 'energy' || p.categorySlug === 'general'
@ -92,9 +111,11 @@ export default function FlipbookCatalog({ products }: FlipbookCatalogProps) {
{
id: "nutrition",
name: "تغذیه تخصصی و رشد توله‌ها",
color: "#8B5CF6",
lightBg: "bg-purple-50",
borderBg: "border-purple-200",
color: "#1E1A2B", // بنفش شبانه
lightBg: "#F2EEF8", // لاواندر شیری
borderBg: "#9B6FB0", // بنفش عمیق
accentColor: "#9B6FB0",
secondaryColor: "#C79ED9", // ارکیده روشن
icon: Baby,
tagline: "شیرهای جایگزین مادر با پروتئین بالا و فرنی‌های تغذیه تکمیلی",
matcher: (p: Product) => (p.category || '').includes("تغذیه") || (p.category || '').includes("مکمل") || p.categorySlug === 'nutrition' || p.categorySlug === 'supplements' || (!p.category && !p.categorySlug)
@ -111,6 +132,8 @@ export default function FlipbookCatalog({ products }: FlipbookCatalogProps) {
color: cat.color,
lightBg: cat.lightBg,
borderBg: cat.borderBg,
accentColor: cat.accentColor || cat.borderBg,
secondaryColor: cat.secondaryColor || cat.color,
icon: cat.icon,
tagline: cat.tagline,
startPage: currentStartPage
@ -170,20 +193,33 @@ export default function FlipbookCatalog({ products }: FlipbookCatalogProps) {
return rawTotal % 2 === 0 ? rawTotal : rawTotal + 1;
}, [orderedProducts.length]);
// Determine if device is currently in single-page mode (mobile viewport < 768px)
const isSinglePageMode = () => {
if (typeof window !== "undefined") {
return window.innerWidth < 768;
}
return false;
};
// 3D Page Flip Forward in Persian (Left page turns over to the right!)
const handleNext = () => {
if (currentPage < TOTAL_PAGES && !isFlipping) {
setIsFlipping(true);
setFlipDirection("next");
const single = isSinglePageMode();
setTimeout(() => {
setCurrentPage(prev => {
if (single) {
return Math.min(prev + 1, TOTAL_PAGES);
}
if (prev === 1) return 2; // From cover (1) to spread (2-3)
return Math.min(prev + 2, TOTAL_PAGES);
});
setIsFlipping(false);
setFlipDirection(null);
}, 650);
}, single ? 350 : 650);
}
};
@ -193,14 +229,19 @@ export default function FlipbookCatalog({ products }: FlipbookCatalogProps) {
setIsFlipping(true);
setFlipDirection("prev");
const single = isSinglePageMode();
setTimeout(() => {
setCurrentPage(prev => {
if (single) {
return Math.max(prev - 1, 1);
}
if (prev <= 3) return 1; // Back to cover
return Math.max(prev - 2, 1);
});
setIsFlipping(false);
setFlipDirection(null);
}, 650);
}, single ? 350 : 650);
}
};
@ -657,18 +698,32 @@ export default function FlipbookCatalog({ products }: FlipbookCatalogProps) {
</div>
{/* Mobile Single Page View with 3D Flip */}
{/* Mobile Single Page View with Touch Gesture Support */}
<div
style={{
aspectRatio: "1 / 1.414"
}}
className="flex md:hidden h-full max-h-full max-w-full bg-white rounded-2xl shadow-2xl overflow-hidden border border-slate-800 relative"
className="flex md:hidden h-full max-h-full max-w-full bg-white rounded-2xl shadow-2xl overflow-hidden border border-slate-800 relative select-none"
>
<div
onClick={() => handleNext()}
className="w-full h-full"
>
<div className="w-full h-full relative">
{renderSinglePage(currentPage)}
{/* Invisible Left & Right Tap Zones for Mobile Navigation */}
<div
onClick={(e) => { e.stopPropagation(); handleNext(); }}
className="absolute left-0 top-0 bottom-0 w-1/4 z-30 cursor-pointer active:bg-black/5 transition-colors"
title="صفحه بعدی"
/>
<div
onClick={(e) => { e.stopPropagation(); handlePrev(); }}
className="absolute right-0 top-0 bottom-0 w-1/4 z-30 cursor-pointer active:bg-black/5 transition-colors"
title="صفحه قبلی"
/>
{/* Subtle Swipe Guidance Hint on Bottom Center */}
<div className="absolute bottom-2 left-1/2 -translate-x-1/2 z-20 pointer-events-none opacity-40 bg-black/40 text-white text-[8px] px-2 py-0.5 rounded-full backdrop-blur-xs font-mono">
کشیدن برای ورق زدن
</div>
</div>
</div>