canina/frontend/application/components/IngredientWiki.tsx
devops 2c47e35153
All checks were successful
Deploy Canina / deploy (push) Successful in 1m8s
fix: IngredientWiki fallback to scientificTerms from API with keywords as benefits
2026-07-14 12:27:43 +00:00

245 lines
12 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 } from "react";
import { Product, IngredientInfo } from "../lib/data/products";
import { motion } from "motion/react";
import { FlaskConical, CheckCircle2, ChevronRight, ChevronLeft, Beaker, Search } from "lucide-react";
import { useSettingsStore } from "../lib/store/settingsStore";
import { productService } from "../lib/services/productService";
import { useRouter } from 'next/navigation';
export default function IngredientWiki() {
const router = useRouter();
const texts = useSettingsStore(state => state.texts);
const scientificTerms = useSettingsStore(state => state.scientificTerms);
const [ingredientsWiki, setIngredientsWiki] = useState<IngredientInfo[]>([]);
const [products, setProducts] = useState<Product[]>([]);
const [searchQuery, setSearchQuery] = useState("");
useEffect(() => {
productService.getProducts({ limit: 999 }).then(res => setProducts(res.data));
}, []);
useEffect(() => {
const raw = texts.ingredients_wiki || texts.ingredients_wiki_text;
if (raw) {
try {
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
if (Array.isArray(parsed)) {
setIngredientsWiki(parsed.map((item: any) => ({
id: item.id,
name: item.name,
description: item.description,
benefits: Array.isArray(item.benefits) ? item.benefits : []
})));
return;
}
} catch (e) {
console.error("Failed to parse ingredients_wiki:", e);
}
}
const terms = Object.values(scientificTerms);
if (terms.length > 0) {
setIngredientsWiki(terms.map((t: any) => {
let benefits: string[] = [];
if (t.keywords) {
try {
const parsed = typeof t.keywords === 'string' ? JSON.parse(t.keywords) : t.keywords;
if (Array.isArray(parsed)) benefits = parsed;
} catch (e) {}
}
return {
id: t.key,
name: t.term,
description: t.definition,
benefits
};
}));
}
}, [texts, scientificTerms]);
return (
<div className="bg-white py-12 px-4 overflow-hidden font-vazir" dir="rtl">
<div className="max-w-7xl mx-auto">
{/* Mobile Navigation & Breadcrumbs */}
<div className="lg:hidden mb-12 space-y-4">
<button
onClick={() => router.back()}
className="flex items-center gap-2 text-medical-gray-500 font-bold hover:text-canina-blue transition-colors"
>
<ChevronRight className="w-5 h-5" />
<span>بازگشت</span>
</button>
<div className="flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest overflow-x-auto whitespace-nowrap pb-2">
<span className="cursor-pointer hover:text-canina-blue" onClick={() => router.push('/')}>کانینا</span>
<ChevronLeft className="w-2.5 h-2.5" />
<span className="text-canina-blue">دانشنامه علمی</span>
</div>
</div>
{/* Desktop Breadcrumbs */}
<div className="hidden lg:flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-12">
<span className="cursor-pointer hover:text-canina-blue" onClick={() => router.push('/')}>خانه</span>
<ChevronLeft className="w-2.5 h-2.5" />
<span className="text-canina-blue">دانشنامه مواد نایاب و ارگانیک</span>
</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">
ما از مواد اولیهای استفاده میکنیم که در سطح دارویی فرآوری شدهاند. در اینجا با قدرت بیولوژیک این ترکیبات آشنا شوید.
</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>
</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-32">
{filtered.map((ing, idx) => {
const hasIngredient = products.filter(p =>
p.main_ingredients?.some(mi => mi.toLowerCase().includes(ing.name.toLowerCase())) ||
p.main_ingredients?.some(mi => mi.toLowerCase().includes(ing.id.replace('-', ' ')))
);
return (
<div key={ing.id} className={`flex flex-col lg:flex-row items-center gap-16 ${idx % 2 === 1 ? 'lg:flex-row-reverse' : ''}`}>
{/* Content */}
<div className="lg:w-1/2 space-y-8">
<motion.div
initial={{ opacity: 0, x: idx % 2 === 1 ? 40 : -40 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
>
<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">
<Beaker className="w-6 h-6" />
</div>
<h3 className="text-3xl font-black text-medical-gray-900">{ing.name}</h3>
</div>
<p className="text-lg text-medical-gray-600 leading-relaxed mb-8">
{ing.description}
</p>
<div className="grid grid-cols-1 gap-4 mb-10">
{ing.benefits.map((benefit, bIdx) => (
<div key={bIdx} className="flex items-start gap-4 p-4 bg-medical-gray-50 rounded-2xl border border-medical-gray-100">
<CheckCircle2 className="w-6 h-6 text-canina-blue flex-shrink-0" />
<span className="text-sm font-bold text-medical-gray-700">{benefit}</span>
</div>
))}
</div>
</motion.div>
</div>
{/* Products Linking */}
<div className="lg:w-1/2 relative">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
className="bg-medical-gray-100 rounded-[4rem] p-12 relative overflow-hidden"
>
<div className="absolute top-0 right-0 p-8">
<div className="text-[10px] font-black text-medical-gray-400 uppercase tracking-[0.3em]">موجود در محصولات</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 relative z-10">
{hasIngredient.length > 0 ? hasIngredient.map(p => (
<div
key={p.id}
onClick={() => router.push(`/shop/${p.slug || p.id}`)}
className="bg-white rounded-[2.5rem] p-6 shadow-xl hover:scale-105 transition-transform cursor-pointer group"
>
<img src={p.image} alt={p.name} className="w-24 h-24 object-contain mx-auto mb-4" referrerPolicy="no-referrer" />
<h4 className="text-sm font-black text-center text-medical-gray-900 group-hover:text-canina-blue transition-colors px-2">{p.name}</h4>
</div>
)) : (
<div className="col-span-full py-8 text-center">
<p className="text-medical-gray-400 font-bold italic mb-3">محصولی با این ماده مؤثره یافت نشد.</p>
<button
onClick={() => router.push('/shop')}
className="text-canina-blue font-black text-sm hover:underline"
>
مشاهده همه محصولات
</button>
</div>
)}
</div>
{/* Decorative backdrop */}
<div className="absolute -bottom-20 -left-20 w-80 h-80 bg-canina-blue rounded-full blur-[100px] opacity-10" />
</motion.div>
</div>
</div>
);
})}
</div>
);
})()}
{/* Quality Badge */}
<div className="mt-32 p-12 bg-medical-gray-900 rounded-[4rem] text-center text-white relative overflow-hidden">
<div className="absolute inset-0 opacity-10 mix-blend-overlay">
<div className="grid grid-cols-12 h-full w-full">
{Array.from({length: 120}).map((_, i) => (
<div key={i} className="border border-white/20" />
))}
</div>
</div>
<div className="relative z-10 max-w-3xl mx-auto">
<h3 className="text-3xl lg:text-5xl font-black mb-8 leading-tight italic">تمام مواد اولیه ما دارای گواهینامه <br /> <span className="text-canina-blue">گرید دارویی اختصاصی</span> هستند.</h3>
<p className="text-white/60 text-lg mb-10">این یعنی استانداردی فراتر از مکملهای معمولی، مشابه استانداردهای تولید دارو در اتحادیه اروپا.</p>
<div className="flex justify-center gap-12">
<div className="flex flex-col items-center">
<div className="text-2xl font-black font-vazir text-canina-blue">۱۰۰٪</div>
<div className="text-[10px] font-bold uppercase tracking-widest text-white/40 mt-1">تست آزمایشگاهی</div>
</div>
<div className="flex flex-col items-center">
<div className="text-2xl font-black font-vazir">GMP</div>
<div className="text-[10px] font-bold uppercase tracking-widest text-white/40 mt-1">استاندارد تولید</div>
</div>
<div className="flex flex-col items-center">
<div className="text-2xl font-black font-vazir">ISO</div>
<div className="text-[10px] font-bold uppercase tracking-widest text-white/40 mt-1">کنترل کیفیت</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}