feat(frontend): create user-facing interactive catalog page (/catalog) with PDF print exporter and remove internal PDF link
This commit is contained in:
parent
abf22cc18d
commit
6932f091c4
236
frontend/application/app/catalog/CatalogClient.tsx
Normal file
236
frontend/application/app/catalog/CatalogClient.tsx
Normal file
@ -0,0 +1,236 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import SafeImage from "../../components/SafeImage";
|
||||
import { Download, Printer, ShieldCheck, Sparkles, CheckCircle2, ChevronLeft, Search, Filter } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { productService } from "../../lib/services/productService";
|
||||
import { Product } from "../../lib/data/products";
|
||||
import { toPersian } from "../../lib/utils";
|
||||
|
||||
export default function CatalogClient() {
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedCategory, setSelectedCategory] = useState("all");
|
||||
|
||||
useEffect(() => {
|
||||
productService.getProducts({ limit: 999 })
|
||||
.then(res => {
|
||||
setProducts(res.data || []);
|
||||
})
|
||||
.catch(err => console.error("Catalog fetch error:", err))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const categories = Array.from(new Set(products.map(p => p.category).filter(Boolean)));
|
||||
|
||||
const filteredProducts = products.filter(p => {
|
||||
const matchCategory = selectedCategory === "all" || p.category === selectedCategory;
|
||||
const matchSearch = p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.artNo.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(p.main_ingredients && p.main_ingredients.some(i => i.toLowerCase().includes(searchQuery.toLowerCase())));
|
||||
return matchCategory && matchSearch;
|
||||
});
|
||||
|
||||
const handlePrint = () => {
|
||||
window.print();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 font-vazir text-slate-800 py-12 px-4 sm:px-8 print:bg-white print:py-0 print:px-0" dir="rtl">
|
||||
{/* Header Bar - Hidden on print */}
|
||||
<div className="max-w-7xl mx-auto mb-10 print:hidden">
|
||||
<div className="bg-gradient-to-r from-canina-blue via-blue-900 to-canina-blue text-white rounded-[2.5rem] p-8 md:p-12 shadow-2xl relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-96 h-96 bg-white/5 rounded-full blur-3xl -mr-20 -mt-20 pointer-events-none" />
|
||||
|
||||
<div className="relative z-10 flex flex-col lg:flex-row lg:items-center justify-between gap-8">
|
||||
<div className="max-w-2xl">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 bg-white/10 rounded-full text-xs font-black uppercase tracking-widest text-amber-300 mb-4 border border-white/10">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<span>کاتالوگ رسمی محصولات کانینا آلمان</span>
|
||||
</div>
|
||||
<h1 className="text-3xl md:text-5xl font-black font-lalezar leading-tight mb-4">
|
||||
راهنمای جامع مکملهای درمانی و دارویی Canina Pharma
|
||||
</h1>
|
||||
<p className="text-white/80 text-sm md:text-base leading-relaxed">
|
||||
معرفی کامل ترکیبات فعال، موارد مصرف بالینی و استانداردهای دارویی محصولات برند آلمانی کانینا جهت استفاده صاحبان پتها و متخصصین دامپزشکی.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<button
|
||||
onClick={handlePrint}
|
||||
className="bg-amber-400 hover:bg-amber-500 text-slate-900 font-black px-6 py-4 rounded-2xl shadow-xl hover:scale-105 transition-all flex items-center gap-3 text-sm cursor-pointer"
|
||||
>
|
||||
<Printer className="w-5 h-5" />
|
||||
<span>چاپ / دانلود نسخه PDF کاتالوگ</span>
|
||||
</button>
|
||||
<Link
|
||||
href="/shop"
|
||||
className="bg-white/10 hover:bg-white/20 text-white font-bold px-6 py-4 rounded-2xl border border-white/20 transition-all flex items-center gap-2 text-sm"
|
||||
>
|
||||
<span>ورود به فروشگاه</span>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Print Cover Header - Visible only on Print */}
|
||||
<div className="hidden print:block text-center py-8 border-b-2 border-canina-blue mb-8">
|
||||
<h1 className="text-3xl font-black text-canina-blue font-lalezar mb-2">کاتالوگ رسمی مکملهای درمانی Canina Pharma Germany</h1>
|
||||
<p className="text-sm text-gray-600 font-bold">واردکننده انحصاری مکملهای دارویی سگ و گربه در ایران — info@canina-iran.com | ۰۲۱-۸۸۸۸۴۴۴۴</p>
|
||||
</div>
|
||||
|
||||
{/* Filter & Search Controls - Hidden on print */}
|
||||
<div className="max-w-7xl mx-auto mb-10 print:hidden flex flex-col md:flex-row gap-4 justify-between items-center bg-white p-6 rounded-3xl border border-slate-200 shadow-sm">
|
||||
<div className="relative w-full md:w-96">
|
||||
<Search className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="جستجوی نام محصول، کد کالا یا ماده موثره..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full bg-slate-50 border border-slate-200 rounded-2xl py-3 pr-11 pl-4 text-sm font-bold focus:outline-none focus:ring-2 focus:ring-canina-blue/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 overflow-x-auto w-full md:w-auto pb-2 md:pb-0">
|
||||
<button
|
||||
onClick={() => setSelectedCategory("all")}
|
||||
className={`px-4 py-2.5 rounded-xl text-xs font-black transition-all shrink-0 ${
|
||||
selectedCategory === "all" ? "bg-canina-blue text-white shadow-md" : "bg-slate-100 text-slate-600 hover:bg-slate-200"
|
||||
}`}
|
||||
>
|
||||
همه دستهها ({products.length})
|
||||
</button>
|
||||
{categories.map(cat => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => setSelectedCategory(cat)}
|
||||
className={`px-4 py-2.5 rounded-xl text-xs font-black transition-all shrink-0 ${
|
||||
selectedCategory === cat ? "bg-canina-blue text-white shadow-md" : "bg-slate-100 text-slate-600 hover:bg-slate-200"
|
||||
}`}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Catalog Products Grid */}
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{loading ? (
|
||||
<div className="text-center py-20 text-slate-400 font-bold">در حال بارگذاری کاتالوگ محصولات...</div>
|
||||
) : filteredProducts.length === 0 ? (
|
||||
<div className="text-center py-20 bg-white rounded-3xl border border-slate-200">
|
||||
<h3 className="text-xl font-black text-slate-700 mb-2">محصولی یافت نشد</h3>
|
||||
<p className="text-sm text-slate-500">لطفاً عبارت جستجو را تغییر دهید.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 print:grid-cols-2 print:gap-6">
|
||||
{filteredProducts.map((product) => (
|
||||
<div
|
||||
key={product.id}
|
||||
className="bg-white rounded-3xl border border-slate-200 overflow-hidden shadow-sm hover:shadow-xl transition-all duration-300 flex flex-col justify-between print:break-inside-avoid print:shadow-none print:border-slate-300"
|
||||
>
|
||||
<div>
|
||||
{/* Top Image Banner */}
|
||||
<div className="relative bg-slate-100 p-6 flex items-center justify-center border-b border-slate-100 aspect-video print:h-44">
|
||||
<SafeImage
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
className="w-full h-full"
|
||||
imgClassName="object-contain max-h-40"
|
||||
/>
|
||||
<div className="absolute top-3 right-3 bg-canina-blue text-white px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-wider">
|
||||
کد کالا: {product.artNo}
|
||||
</div>
|
||||
{product.suitableFor && (
|
||||
<div className="absolute bottom-3 left-3 bg-white/90 backdrop-blur-sm text-slate-800 px-2.5 py-1 rounded-lg text-[11px] font-black shadow-sm">
|
||||
مناسب: {product.suitableFor}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Body Content */}
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<span className="text-[10px] font-black text-canina-blue uppercase tracking-widest block mb-1">
|
||||
{product.category}
|
||||
</span>
|
||||
<h3 className="text-xl font-black text-slate-900 leading-snug font-vazir">
|
||||
{product.name}
|
||||
</h3>
|
||||
{product.scientificTagline && (
|
||||
<p className="text-xs text-slate-500 font-bold mt-1 leading-relaxed">
|
||||
{product.scientificTagline}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-600 leading-relaxed line-clamp-4 print:line-clamp-none">
|
||||
{product.shortDescription || product.description}
|
||||
</p>
|
||||
|
||||
{/* Ingredients Badges */}
|
||||
{product.main_ingredients && product.main_ingredients.length > 0 && (
|
||||
<div className="pt-2 border-t border-slate-100">
|
||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-wider block mb-2">
|
||||
مواد موثره و ترکیبات اصلی:
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{product.main_ingredients.map((ing, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="bg-blue-50 text-canina-blue border border-blue-100 rounded-lg px-2 py-1 text-[10px] font-bold"
|
||||
>
|
||||
{ing}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dosage Instructions */}
|
||||
{product.dosageLogic && (
|
||||
<div className="bg-amber-50/60 border border-amber-200/60 rounded-2xl p-3 text-xs font-bold text-amber-900 flex items-start gap-2">
|
||||
<ShieldCheck className="w-4 h-4 text-amber-600 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<span className="font-black text-[11px] block text-amber-800">دستور مصرف بالینی:</span>
|
||||
<span className="text-[11px] font-medium leading-relaxed">{product.dosageLogic}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card Footer - Hidden on print */}
|
||||
<div className="p-4 bg-slate-50 border-t border-slate-100 flex items-center justify-between print:hidden">
|
||||
<div className="flex items-center gap-1 text-[11px] font-bold text-slate-500">
|
||||
<CheckCircle2 className="w-4 h-4 text-green-600" />
|
||||
<span>گرید دارویی آلمان</span>
|
||||
</div>
|
||||
<Link
|
||||
href={`/shop/${product.slug || product.id}`}
|
||||
className="text-xs font-black text-canina-blue hover:underline flex items-center gap-1"
|
||||
>
|
||||
<span>جزئیات تخصصی</span>
|
||||
<ChevronLeft className="w-3.5 h-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Print Footer */}
|
||||
<div className="hidden print:block text-center pt-8 border-t border-slate-300 mt-12 text-xs text-slate-500 font-bold">
|
||||
صفحه کاتالوگ آنلاین محصولات Canina Pharma Germany — وبسایت رسمی: https://canina-iran.com
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
22
frontend/application/app/catalog/page.tsx
Normal file
22
frontend/application/app/catalog/page.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import type { Metadata } from "next";
|
||||
import CatalogClient from "./CatalogClient";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "کاتالوگ محصولات Canina - راهنمای مکملهای درمانی سگ و گربه",
|
||||
description: "کاتالوگ جامع محصولات آلمانی کانینا شامل راهنمای ترکیبات، گرید دارویی، دوز مصرفی و موارد درمان بالینی مفصل، پوست و مو و رشد پتها.",
|
||||
alternates: {
|
||||
canonical: "/catalog",
|
||||
},
|
||||
openGraph: {
|
||||
title: "کاتالوگ رسمی مکملهای درمانی Canina Pharma Germany",
|
||||
description: "دانلود و مشاهده آنلاین کاتالوگ مکملهای دارویی حیوانات خانگی کانینا آلمان",
|
||||
url: "https://canina-iran.com/catalog",
|
||||
siteName: "کانینا ایران",
|
||||
locale: "fa_IR",
|
||||
type: "website",
|
||||
}
|
||||
};
|
||||
|
||||
export default function CatalogPage() {
|
||||
return <CatalogClient />;
|
||||
}
|
||||
@ -95,15 +95,13 @@ export default function Footer({ onNavigate, onShopNavigate, onB2BOpen }: {
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/canina.pdf"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<Link
|
||||
href="/catalog"
|
||||
className="hover:text-canina-gold hover:translate-x-[-4px] transition-all cursor-pointer flex items-center gap-2 group text-amber-200"
|
||||
>
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-canina-gold group-hover:w-4 transition-all" />
|
||||
<span>دانلود کاتالوگ رسمی Canina (PDF)</span>
|
||||
</a>
|
||||
<span>دانلود کاتالوگ آنلاین محصولات (PDF)</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li onClick={onB2BOpen} className="hover:text-canina-blue hover:translate-x-[-4px] transition-all cursor-pointer flex items-center gap-2 group text-blue-200">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-canina-blue group-hover:w-4 transition-all" />
|
||||
|
||||
@ -233,6 +233,7 @@ export default function Header({
|
||||
|
||||
{[
|
||||
{ id: 'shop', label: getText('nav_products', 'محصولات تخصصی'), href: '/shop' },
|
||||
{ id: 'catalog', label: getText('nav_catalog', 'کاتالوگ آنلاین'), href: '/catalog' },
|
||||
{ id: 'wiki', label: getText('nav_wiki', 'دانشنامه علمی'), href: '/wiki' },
|
||||
{ id: 'blog', label: getText('nav_blog', 'مجله سلامت پت'), href: '/blog' },
|
||||
{ id: 'profile', label: getText('nav_pet_profiles', 'شناسنامه پت'), href: '/profile' }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user