canina/frontend/application/components/IngredientWiki.tsx
parsa aghaei 2d83a00169
All checks were successful
Deploy Canina / deploy (push) Successful in 1m54s
feat(ui): optimize wiki ingredient links, product images, back buttons, and checkout settings
2026-08-17 14:43:36 +03:30

348 lines
20 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useEffect, useMemo } from "react";
import { Product, IngredientInfo, PRODUCTS, INGREDIENTS_WIKI } from "../lib/data/products";
import { motion } from "motion/react";
import { FlaskConical, CheckCircle2, ChevronRight, ChevronLeft, Beaker, Search } from "lucide-react";
import { productService } from "../lib/services/productService";
import Link from "next/link";
import Image from "next/image";
import { useRouter, useSearchParams } from 'next/navigation';
import api from "../lib/services/api";
import { Ingredient } from "../lib/types";
// Keywords map for connecting ingredients to products
const INGREDIENT_MATCH_KEYWORDS: Record<string, string[]> = {
"green-mussel": ["صدف", "mussel", "gag", "perna", "canhydrox", "velox", "flexan", "gelenkenergie"],
"hydroxyapatite": ["هیدروکسی", "آپاتیت", "hydroxyapatite", "canhydrox"],
"collagen": ["کلاژن", "collagen", "flexan"],
"colostrum": ["آغوز", "colostrum", "immun-booster", "ایمون بوستر"],
"silver": ["نقره", "میکروسیلور", "silver", "zahngel", "ژل دندان", "دندان"],
"peat-extract": ["پیت", "moor", "moortranke", "هومیک", "عصاره پیت", "گوارش"],
"salmon-oil": ["سالمون", "ماهی", "salmon", "lachs", "امگا", "marine", "welpenmilch", "katzenmilch"],
"black-cumin": ["سیاه دانه", "سیاهدانه", "زیره", "cumin", "schwarz", "kummel"],
"eggshell": ["پوسته", "تخم مرغ", "تخم‌مرغ", "eggshell", "eierschalen"],
"biotin": ["بیوتین", "biotin", "vitamin h", "ویتامین h"],
"taurin": ["تورین", "taurin", "taurine", "katzenmilch"],
"seaweed": ["جلبک", "seaweed", "seealgen", "seelgen", "ascophyllum", "canhydrox"],
"l-carnitine": ["کارنیتین", "carnitine", "herz-vital", "هرز ویتال", "قلب"],
"hawthorn": ["زالزالک", "hawthorn", "crataegus", "herz-vital", "هرز ویتال", "قلب"],
"brewer-yeast": ["مخمر", "yeast", "hefe", "b-complex", "canhydrox", "mineral-tabs", "vitamin-tabs", "taurin", "hefe-tabletten"],
"bovine-blood": ["خون", "rinderblut", "blood", "آهن", "هموگلوبین", "سرم"],
"bovine-fat": ["چربی", "rinderfett", "fat", "کالری", "گوشت گاو", "energy-gel", "immun-booster", "katzenmilch", "welpenmilch"],
"willow-bark": ["بید", "willow", "salix", "arthro", "آرترو"],
"ginger": ["زنجبیل", "ginger", "zingiber", "arthro", "آرترو"],
"hyaluronic": ["هیالورونات", "hyaluron", "augenpflege", "چشم"],
"calendula-sea-buckthorn": ["کالاندولا", "سنجد", "خولان", "pfotenpflege", "پنجه", "همیشه‌بهار", "پوست"],
"dodecanoic": ["دودکانوئیک", "مارگوسا", "insect", "کک", "کنه", "حشرات", "ضد انگل", "پپت-پروتکت", "pet-protect", "اسپری"],
"calcium-carbonate": ["کربنات کلسیم", "کلسیم", "calcium-carbonat", "mineral-tabs", "welpenkalk", "gag", "vitamin-tabs", "ballaststoff"],
"vitamin-c": ["ویتامین c", "ویتامین سی", "ascorbic", "vitamin-c", "zahngel", "canhydrox"],
"vitamin-e": ["ویتامین e", "ویتامین ای", "tocopherol", "vitamin-e", "canhydrox", "immun-booster", "energy-gel", "gag", "arthro"],
"vitamin-d3": ["ویتامین d3", "ویتامین دی", "cholecalciferol", "d3", "immun-booster", "mineral-tabs", "vitamin-tabs", "welpenmilch"],
"dextrose": ["دکستروز", "dextrose", "گلوکز", "biotin-tabs", "herz-vital", "energy-gel"],
"marine-oils": ["گردو", "آرگان", "گل رز", "marine-ol", "olmischung", "روغن دریایی", "روغن"],
"apple-fiber": ["سیب", "پکتین", "فیبر", "ballaststoff", "fiber", "یونجه", "هویج", "نخود"],
"zinc": ["روی", "zinc", "کلات", "arthro", "seealgen", "biotin", "mineral-tabs"]
};
// Formatter to prevent duplicate English names like "آغوز (Colostrum) (Colostrum)"
function formatIngredientDisplayName(item: { nameFa?: string; scientificName?: string; nameEn?: string; name?: string; slug?: string }): string {
const rawName = item.nameFa || item.name || '';
const cleanFa = rawName.replace(/\s*\([^)]*\)\s*/g, ' ').replace(/\s+/g, ' ').trim();
const sub = (item.scientificName || item.nameEn || '').replace(/\s*\([^)]*\)\s*/g, ' ').replace(/\s+/g, ' ').trim();
if (!cleanFa) return sub || rawName || item.slug || '';
if (!sub || cleanFa.toLowerCase() === sub.toLowerCase() || /[\u0600-\u06FF]/.test(sub)) {
return cleanFa;
}
return `${cleanFa} (${sub})`;
}
export default function IngredientWiki() {
const router = useRouter();
const searchParams = useSearchParams();
const termParam = searchParams.get('term');
const [ingredientsWiki, setIngredientsWiki] = useState<IngredientInfo[]>(INGREDIENTS_WIKI);
const [products, setProducts] = useState<Product[]>(PRODUCTS);
const [searchQuery, setSearchQuery] = useState("");
useEffect(() => {
productService.getProducts({ limit: 999 })
.then(res => {
if (res?.data && res.data.length > 0) {
setProducts(res.data);
}
})
.catch(err => {
console.warn('[IngredientWiki] Using static fallback products:', err);
});
}, []);
useEffect(() => {
api.get('/ingredients')
.then(res => {
const rawList = Array.isArray(res.data) ? res.data : (res.data?.data || []);
if (rawList.length > 0) {
const apiList: IngredientInfo[] = rawList.map((item: Ingredient) => ({
id: item.slug || item.id,
name: formatIngredientDisplayName(item),
description: item.description || '',
benefits: Array.isArray(item.benefits) ? item.benefits : []
}));
setIngredientsWiki(apiList);
}
})
.catch((err) => {
console.warn('[IngredientWiki] Using static INGREDIENTS_WIKI fallback:', err);
});
}, []);
useEffect(() => {
if (termParam && ingredientsWiki.length > 0) {
const el = document.getElementById(`wiki-${termParam}`);
if (el) {
setTimeout(() => {
const yOffset = -120;
const y = el.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({ top: y, behavior: 'smooth' });
}, 200);
}
}
}, [termParam, ingredientsWiki]);
// Product matcher for each ingredient
const getProductsForIngredient = useMemo(() => {
return (ingId: string, ingName: string) => {
const cleanSlug = (ingId || '').replace(/-/g, ' ').toLowerCase();
const cleanFa = (ingName || '').replace(/\s*\([^)]*\)\s*/g, ' ').trim().toLowerCase();
const keywords = INGREDIENT_MATCH_KEYWORDS[ingId] || [];
return products.filter(p => {
const pText = [
p.name || '',
p.nameFa || '',
p.nameEn || '',
p.scientificTagline || '',
p.slug || '',
p.id || '',
p.description || '',
p.shortDescription || '',
p.category || '',
p.categorySlug || '',
...(p.main_ingredients || []),
...(p.benefitsList || []),
...(p.keyBenefits?.map(k => `${k.title} ${k.description}`) || []),
(p as any).ingredients || ''
].join(' ').toLowerCase();
if (cleanFa && pText.includes(cleanFa)) return true;
if (cleanSlug && pText.includes(cleanSlug)) return true;
return keywords.some(kw => pText.includes(kw.toLowerCase()));
});
};
}, [products]);
return (
<div className="bg-white py-12 px-4 overflow-hidden font-vazir" dir="rtl">
<div className="max-w-7xl mx-auto">
{/* Standard Breadcrumb Navigation & Integrated Back Button */}
<div className="flex items-center justify-between mb-8 pb-4 border-b border-medical-gray-200/60 font-vazir" dir="rtl">
<div className="flex items-center gap-2 text-xs font-bold text-medical-gray-400 whitespace-nowrap">
<span className="cursor-pointer hover:text-canina-blue transition-colors" onClick={() => router.push('/')}>خانه</span>
<ChevronLeft className="w-3 h-3 text-medical-gray-300" />
<span className="text-canina-blue font-black">دانشنامه مواد نایاب و ارگانیک</span>
</div>
<button
onClick={() => router.push('/')}
className="px-3.5 py-1.5 bg-white border border-medical-gray-200 rounded-lg text-medical-gray-600 shadow-xs flex items-center gap-1.5 font-bold text-xs hover:border-canina-blue hover:text-canina-blue transition-all whitespace-nowrap cursor-pointer"
>
<span>بازگشت</span>
<ChevronLeft className="w-3.5 h-3.5" />
</button>
</div>
<div className="flex flex-col items-center text-center mb-20">
<div className="inline-flex items-center gap-2 px-4 py-2 bg-canina-blue/5 border border-canina-blue/10 rounded-full text-canina-blue text-xs font-black uppercase tracking-widest mb-6">
<FlaskConical className="w-4 h-4" />
علم در خدمت کیفیت
</div>
<h2 className="text-4xl lg:text-6xl font-black text-medical-gray-900 leading-tight md:max-w-4xl">
دانشنامه مواد <span className="text-canina-blue italic">نایاب و ارگانیک</span> کنینا
</h2>
<p className="mt-6 text-lg text-medical-gray-500 max-w-2xl leading-relaxed">
تمامی ترکیبات فعال و مکمل‌های به کار رفته در محصولات دارویی و مراقبتی Canina Pharma آلمان، دارای بالاترین گرید بیولوژیک و فارماکوپه اروپا می‌باشند.
</p>
<div className="relative w-full max-w-md mt-8">
<Search className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-medical-gray-400" />
<input
type="text"
placeholder="جستجوی ماده مؤثره (مثلاً: صدف لب‌سبز، آغوز، بیوتین)..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-full py-3 pr-11 pl-4 text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20"
/>
</div>
{/* Sugar-Free & Gluten-Free Medical Standards Banner */}
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
<div className="px-4 py-2 bg-emerald-50 border border-emerald-200 text-emerald-800 rounded-2xl text-xs font-black flex items-center gap-1.5 shadow-xs">
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
<span>۱۰۰٪ فاقد قند افزوده (Sugar-Free)</span>
</div>
<div className="px-4 py-2 bg-blue-50 border border-blue-200 text-blue-800 rounded-2xl text-xs font-black flex items-center gap-1.5 shadow-xs">
<CheckCircle2 className="w-4 h-4 text-blue-600" />
<span>مطابق استانداردهای دارویی و بالینی آلمان</span>
</div>
</div>
</div>
{(() => {
const filtered = ingredientsWiki.filter(ing => {
if (!searchQuery.trim()) return true;
const q = searchQuery.trim().toLowerCase();
return ing.name.toLowerCase().includes(q) || ing.description.toLowerCase().includes(q);
});
if (filtered.length === 0) {
return (
<div className="text-center py-20">
<Search className="w-12 h-12 text-medical-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-black text-medical-gray-900 mb-2">ماده‌ای یافت نشد</h3>
<p className="text-medical-gray-400 font-bold">با عبارت جستجو شده ماده مؤثره‌ای مطابقت ندارد.</p>
</div>
);
}
return (
<div className="space-y-24">
{filtered.map((ing, idx) => {
const hasIngredient = getProductsForIngredient(ing.id, ing.name);
const isTargeted = termParam === ing.id;
return (
<div
key={ing.id}
id={`wiki-${ing.id}`}
className={`flex flex-col lg:flex-row items-stretch gap-12 p-8 sm:p-12 rounded-[3.5rem] transition-all duration-500 border border-medical-gray-100 shadow-sm hover:shadow-xl ${isTargeted ? 'bg-canina-blue/5 border-2 border-canina-blue ring-4 ring-canina-blue/10 shadow-2xl' : 'bg-white'} ${idx % 2 === 1 ? 'lg:flex-row-reverse' : ''}`}
>
{/* Content */}
<div className="lg:w-1/2 flex flex-col justify-between space-y-6">
<motion.div
initial={{ opacity: 0, x: idx % 2 === 1 ? 40 : -40 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<div className="flex items-center gap-4 mb-6">
<div className="w-14 h-14 rounded-3xl bg-medical-gray-900 flex items-center justify-center text-white shadow-xl shrink-0">
<Beaker className="w-6 h-6 text-canina-gold" />
</div>
<div>
<h3 className="text-2xl sm:text-3xl font-black text-medical-gray-900 leading-tight font-vazir">{ing.name}</h3>
<span className="text-[12px] text-canina-blue font-bold tracking-wider font-vazir inline-block mt-1 bg-canina-blue/5 px-2.5 py-0.5 rounded-lg border border-canina-blue/10">کد ماده مؤثره: <span className="font-mono">{ing.id}</span></span>
</div>
</div>
<p className="text-base text-medical-gray-600 leading-relaxed mb-6 font-vazir">
{ing.description}
</p>
<div className="grid grid-cols-1 gap-3 mb-6">
{ing.benefits.map((benefit, bIdx) => (
<div key={bIdx} className="flex items-start gap-3 p-3.5 bg-medical-gray-50/80 rounded-2xl border border-medical-gray-100">
<CheckCircle2 className="w-5 h-5 text-canina-blue shrink-0 mt-0.5" />
<span className="text-xs sm:text-sm font-bold text-medical-gray-700 leading-relaxed font-vazir">{benefit}</span>
</div>
))}
</div>
</motion.div>
</div>
{/* Products Linking */}
<div className="lg:w-1/2 relative flex">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
className="bg-medical-gray-50/80 rounded-[3rem] p-6 sm:p-8 border border-medical-gray-200/60 relative overflow-hidden flex-1 flex flex-col justify-between"
>
<div>
<div className="flex items-center justify-between mb-6 pb-4 border-b border-medical-gray-200/60">
<div className="flex items-center gap-2">
<span className="w-2.5 h-2.5 rounded-full bg-canina-blue animate-pulse" />
<h4 className="text-xs sm:text-sm font-black text-medical-gray-900 font-vazir">موجود در محصولات کاتالوگ</h4>
</div>
<Link
href={`/shop?search=${encodeURIComponent(ing.name.replace(/\s*\([^)]*\)\s*/g, '').trim())}`}
className="text-xs font-black text-canina-blue bg-canina-blue/10 hover:bg-canina-blue hover:text-white px-3 py-1 rounded-full font-vazir transition-all"
>
{hasIngredient.length} محصول
</Link>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 relative z-10">
{hasIngredient.length > 0 ? hasIngredient.slice(0, 4).map(p => (
<Link
key={p.id}
href={`/shop/${p.slug || p.id}`}
className="bg-white rounded-[2rem] p-4 shadow-sm border border-medical-gray-200 hover:border-canina-blue hover:shadow-lg hover:scale-[1.02] transition-all cursor-pointer group flex flex-col justify-between"
>
<div className="aspect-square bg-medical-gray-50 rounded-2xl p-3 mb-3 flex items-center justify-center relative overflow-hidden">
<Image
src={p.image || '/assets/images/canina-product-placeholder.png'}
alt={p.name}
width={96}
height={96}
className="max-h-20 w-auto object-contain group-hover:scale-110 transition-transform duration-300"
unoptimized
onError={(e: any) => { e.currentTarget.src = '/assets/images/canina-product-placeholder.png'; }}
/>
</div>
<h4 className="text-xs font-black text-center text-medical-gray-900 group-hover:text-canina-blue transition-colors px-1 leading-snug font-vazir line-clamp-2">{p.name}</h4>
</Link>
)) : (
<div className="col-span-full py-8 text-center">
<p className="text-medical-gray-400 font-bold italic mb-3 font-vazir">محصولی با این ماده مؤثره یافت نشد.</p>
<button
onClick={() => router.push('/shop')}
className="text-canina-blue font-black text-sm hover:underline cursor-pointer font-vazir"
>
مشاهده همه محصولات
</button>
</div>
)}
</div>
</div>
{hasIngredient.length > 4 && (
<div className="mt-4 pt-3 border-t border-medical-gray-200/50 text-center">
<Link
href={`/shop?search=${encodeURIComponent(ing.name.replace(/\s*\([^)]*\)\s*/g, '').trim())}`}
className="text-xs font-black text-canina-blue hover:text-medical-gray-900 transition-colors font-vazir inline-flex items-center gap-1.5"
>
<span>+ و {hasIngredient.length - 4} محصول تخصصی دیگر در کاتالوگ</span>
<span className="underline">مشاهده همه</span>
</Link>
</div>
)}
{/* Decorative backdrop */}
<div className="absolute -bottom-20 -left-20 w-80 h-80 bg-canina-blue rounded-full blur-[100px] opacity-10 pointer-events-none" />
</motion.div>
</div>
</div>
);
})}
</div>
);
})()}
</div>
</div>
);
}