canina/frontend/application/components/BlogPage.tsx
parsa aghaei 1ddfb0a823
All checks were successful
Deploy Canina / deploy (push) Successful in 1m37s
fix(blog): update fallback image handling, use SafeImage, and enhance related products display
2026-08-24 17:49:40 +03:30

443 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 React, { useState, useMemo } from "react";
import { motion, AnimatePresence } from "motion/react";
import {
ChevronRight,
ChevronLeft,
Calendar,
User,
ArrowLeft,
Sparkles,
BookOpen,
Mail,
Check,
Clock,
Tag,
Search,
Pin,
Eye,
Filter,
} from "lucide-react";
import { toast } from "sonner";
import { Product } from "../lib/data/products";
import { productService } from "../lib/services/productService";
import { useRouter } from 'next/navigation';
import Image from 'next/image';
import SafeImage from './SafeImage';
export interface BlogCategory {
id: string;
name: string;
slug: string;
}
export interface BlogPostItem {
id: string;
slug: string;
title: string;
excerpt: string;
image: string;
imageAlt?: string;
date: string;
author: string;
content: string;
category?: string;
categoryId?: string;
tags?: { id: string; name: string }[];
readingTime?: number;
featured?: boolean;
viewCount?: number;
}
export default function BlogPage({
blogs = [],
categories = [],
}: {
blogs: BlogPostItem[];
categories?: BlogCategory[];
}) {
const router = useRouter();
const [products, setProducts] = useState<Product[]>([]);
const [selectedCategory, setSelectedCategory] = useState<string>("ALL");
const [searchQuery, setSearchQuery] = useState<string>("");
const [selectedTag, setSelectedTag] = useState<string | null>(null);
React.useEffect(() => {
productService.getProducts({ limit: 8 }).then((res) => setProducts(res.data));
}, []);
// Filtered blogs list
const filteredBlogs = useMemo(() => {
return blogs.filter((b) => {
const matchCategory =
selectedCategory === "ALL" ||
b.categoryId === selectedCategory ||
b.category === selectedCategory;
const matchSearch =
!searchQuery.trim() ||
b.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
b.excerpt.toLowerCase().includes(searchQuery.toLowerCase());
const matchTag =
!selectedTag ||
(b.tags && b.tags.some((t) => t.name === selectedTag));
return matchCategory && matchSearch && matchTag;
});
}, [blogs, selectedCategory, searchQuery, selectedTag]);
// Featured post (prioritize featured flag, otherwise the first item)
const featuredPost = useMemo(() => {
if (filteredBlogs.length === 0) return null;
const explicitlyFeatured = filteredBlogs.find((b) => b.featured);
return explicitlyFeatured || filteredBlogs[0];
}, [filteredBlogs]);
// Regular posts excluding featured if displayed prominently
const regularPosts = useMemo(() => {
if (!featuredPost) return filteredBlogs;
return filteredBlogs.filter((b) => b.id !== featuredPost.id);
}, [filteredBlogs, featuredPost]);
// Extract all unique tags
const allTags = useMemo(() => {
const map = new Map<string, string>();
blogs.forEach((b) => {
b.tags?.forEach((t) => {
map.set(t.name, t.name);
});
});
return Array.from(map.values());
}, [blogs]);
return (
<div className="min-h-screen bg-medical-gray-50 pt-8 pb-24 px-4 font-vazir" dir="rtl">
<div className="max-w-7xl mx-auto">
{/* Breadcrumb Navigation */}
<div className="flex items-center justify-between mb-8 pb-4 border-b border-medical-gray-200/60 font-vazir">
<div className="flex items-center gap-2 text-xs font-bold text-medical-gray-400">
<span className="cursor-pointer hover:text-canina-blue transition-colors" onClick={() => router.push('/')}>
خانه
</span>
<ChevronRight 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>
{/* Page Header */}
<div className="mb-10 text-right">
<div className="inline-flex items-center gap-2 px-3.5 py-1.5 bg-canina-blue/10 text-canina-blue rounded-full text-xs font-black uppercase tracking-widest mb-3 shadow-2xs">
<BookOpen className="w-4 h-4" />
<span>دانشنامه علمی و درمانی کنینا آلمان</span>
</div>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-black text-medical-gray-900 leading-tight">
مجله تخصصی <span className="text-canina-blue">سلامت و تغذیه سگ و گربه</span>
</h1>
<p className="text-sm sm:text-base text-medical-gray-500 font-medium mt-3 max-w-2xl leading-relaxed">
جدیدترین یافتههای بالینی، مقالات تخصصی دامپزشکی و پروتکلهای پیشگیری و درمان بر پایه استانداردهای علمی کنینا آلمان.
</p>
</div>
{/* Search & Category Filter Section */}
<div className="bg-white p-6 rounded-3xl border border-medical-gray-200 shadow-sm mb-12 space-y-5">
<div className="flex flex-col md:flex-row items-center gap-4">
{/* Search Input */}
<div className="relative flex-1 w-full">
<Search className="w-4 h-4 absolute right-4 top-1/2 -translate-y-1/2 text-medical-gray-400" />
<input
type="text"
placeholder="جستجو در موضوعات، بیماری‌ها، ویتامین‌ها..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-4 pr-11 py-3 rounded-2xl bg-medical-gray-50 border border-medical-gray-200 focus:border-canina-blue focus:bg-white text-xs sm:text-sm font-bold outline-none transition-all"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery("")}
className="absolute left-3 top-1/2 -translate-y-1/2 text-xs font-bold text-medical-gray-400 hover:text-medical-gray-600"
>
پاک کردن
</button>
)}
</div>
{selectedTag && (
<div className="flex items-center gap-2 px-3 py-2 bg-purple-50 text-purple-700 border border-purple-200 rounded-xl text-xs font-bold">
<span>تگ انتخاب شده: #{selectedTag}</span>
<button
onClick={() => setSelectedTag(null)}
className="text-purple-500 hover:text-purple-900"
>
</button>
</div>
)}
</div>
{/* Category Filter Pills */}
<div className="flex items-center gap-2 overflow-x-auto pb-1 scrollbar-none">
<button
onClick={() => setSelectedCategory("ALL")}
className={`px-4 py-2 rounded-2xl text-xs font-black transition-all cursor-pointer whitespace-nowrap ${
selectedCategory === "ALL"
? "bg-canina-blue text-white shadow-md shadow-canina-blue/20 scale-102"
: "bg-medical-gray-100 text-medical-gray-600 hover:bg-medical-gray-200"
}`}
>
همه دستهها ({blogs.length})
</button>
{categories.map((c) => (
<button
key={c.id}
onClick={() => setSelectedCategory(c.id)}
className={`px-4 py-2 rounded-2xl text-xs font-black transition-all cursor-pointer whitespace-nowrap ${
selectedCategory === c.id
? "bg-canina-blue text-white shadow-md shadow-canina-blue/20 scale-102"
: "bg-medical-gray-100 text-medical-gray-600 hover:bg-medical-gray-200"
}`}
>
{c.name}
</button>
))}
</div>
{/* Quick Tag Cloud */}
{allTags.length > 0 && (
<div className="flex flex-wrap items-center gap-1.5 pt-2 border-t border-medical-gray-100">
<span className="text-[11px] font-bold text-medical-gray-400 flex items-center gap-1 ml-2">
<Tag className="w-3 h-3" />
محبوبترین تگها:
</span>
{allTags.slice(0, 10).map((tag) => (
<button
key={tag}
onClick={() => setSelectedTag(selectedTag === tag ? null : tag)}
className={`text-[11px] font-bold px-2.5 py-1 rounded-lg transition-all cursor-pointer ${
selectedTag === tag
? "bg-purple-600 text-white"
: "bg-medical-gray-100 text-medical-gray-600 hover:bg-medical-gray-200"
}`}
>
#{tag}
</button>
))}
</div>
)}
</div>
{/* Featured Post Banner */}
{featuredPost && (
<div className="mb-16">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-[3.5rem] overflow-hidden border border-medical-gray-200 shadow-2xl flex flex-col lg:flex-row cursor-pointer group hover:border-canina-blue/30 transition-all"
onClick={() => router.push(`/blog/${featuredPost.slug}`)}
>
<div className="lg:w-1/2 aspect-video lg:aspect-auto relative overflow-hidden bg-medical-gray-100">
<SafeImage
src={featuredPost.image}
alt={featuredPost.imageAlt || featuredPost.title}
priority
sizes="(max-width: 1024px) 100vw, 50vw"
className="w-full h-full"
imgClassName="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
/>
{featuredPost.featured && (
<div className="absolute top-6 right-6 px-3.5 py-1.5 bg-amber-500 text-white rounded-full text-xs font-black flex items-center gap-1.5 shadow-lg backdrop-blur-md">
<Pin className="w-3.5 h-3.5 fill-white" />
<span>مقاله ویژه و برگزیده</span>
</div>
)}
</div>
<div className="lg:w-1/2 p-8 sm:p-12 flex flex-col justify-center">
<div className="flex flex-wrap items-center gap-3 text-xs font-bold text-canina-blue mb-5">
<div className="bg-canina-blue/10 px-3.5 py-1 rounded-full uppercase tracking-wider">
{featuredPost.category || "پژوهش بالینی"}
</div>
<div className="flex items-center gap-1 text-medical-gray-400">
<Calendar className="w-3.5 h-3.5" />
{featuredPost.date}
</div>
<div className="flex items-center gap-1 text-medical-gray-400">
<Clock className="w-3.5 h-3.5" />
{featuredPost.readingTime || 4} دقیقه مطالعه
</div>
</div>
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-black text-medical-gray-900 mb-5 leading-tight group-hover:text-canina-blue transition-colors">
{featuredPost.title}
</h2>
<p className="text-sm sm:text-base text-medical-gray-500 mb-8 leading-relaxed line-clamp-3">
{featuredPost.excerpt}
</p>
<div className="flex items-center justify-between pt-6 border-t border-medical-gray-100">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-medical-gray-100 rounded-full flex items-center justify-center text-medical-gray-400">
<User className="w-5 h-5" />
</div>
<div className="text-xs">
<div className="font-black text-medical-gray-900">{featuredPost.author}</div>
<div className="font-bold text-medical-gray-400">تیم پژوهش کنینا</div>
</div>
</div>
<div className="flex items-center gap-2 text-canina-blue font-black text-sm">
<span>مطالعه مقاله کامل</span>
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-2 transition-transform" />
</div>
</div>
</div>
</motion.div>
</div>
)}
{/* Regular Posts Grid */}
{regularPosts.length > 0 ? (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{regularPosts.map((post, idx) => (
<motion.div
key={post.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: idx * 0.05 }}
className="bg-white rounded-[2.5rem] overflow-hidden border border-medical-gray-200 shadow-lg group hover:shadow-2xl hover:border-canina-blue/30 transition-all cursor-pointer flex flex-col justify-between"
onClick={() => router.push(`/blog/${post.slug}`)}
>
<div>
<div className="aspect-[16/10] overflow-hidden relative bg-medical-gray-100">
<SafeImage
src={post.image}
alt={post.imageAlt || post.title}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="w-full h-full"
imgClassName="w-full h-full object-cover group-hover:scale-108 transition-transform duration-700"
/>
{post.category && (
<span className="absolute bottom-3 right-3 px-3 py-1 bg-black/60 backdrop-blur-md text-white rounded-xl text-[10px] font-bold z-10">
{post.category}
</span>
)}
</div>
<div className="p-6 sm:p-7">
<div className="flex items-center justify-between text-[11px] font-bold text-medical-gray-400 mb-3">
<div className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
{post.date}
</div>
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{post.readingTime || 3} دقیقه
</div>
</div>
<h3 className="text-lg font-black text-medical-gray-900 mb-3 group-hover:text-canina-blue transition-colors line-clamp-2 leading-snug">
{post.title}
</h3>
<p className="text-xs text-medical-gray-500 line-clamp-3 leading-relaxed mb-4">
{post.excerpt}
</p>
</div>
</div>
<div className="px-6 sm:px-7 pb-6 pt-3 border-t border-medical-gray-100 flex items-center justify-between">
<div className="text-[11px] font-bold text-medical-gray-500 flex items-center gap-1.5">
<User className="w-3.5 h-3.5 text-medical-gray-400" />
<span>{post.author}</span>
</div>
<div className="flex items-center gap-1 text-canina-blue font-black text-xs group-hover:underline">
<span>مطالعه</span>
<ArrowLeft className="w-3.5 h-3.5 group-hover:-translate-x-1 transition-transform" />
</div>
</div>
</motion.div>
))}
</div>
) : (
<div className="text-center py-16 bg-white rounded-3xl border border-medical-gray-200">
<p className="text-medical-gray-500 font-bold text-sm">
هیچ مقالهای با معیارهای جستجوی شما یافت نشد.
</p>
<button
onClick={() => {
setSelectedCategory("ALL");
setSearchQuery("");
setSelectedTag(null);
}}
className="mt-3 text-xs text-canina-blue font-black hover:underline cursor-pointer"
>
پاکسازی همه فیلترها
</button>
</div>
)}
{/* Newsletter Section */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.7 }}
className="mt-20 bg-gradient-to-br from-canina-blue via-canina-blue/95 to-medical-gray-900 rounded-[3rem] p-8 sm:p-14 text-white relative overflow-hidden font-vazir shadow-2xl border border-white/10"
dir="rtl"
>
<div className="absolute top-0 right-0 w-96 h-96 bg-canina-teal/15 rounded-full blur-[100px] pointer-events-none" />
<div className="absolute bottom-0 left-0 w-80 h-80 bg-canina-gold/10 rounded-full blur-[90px] pointer-events-none" />
<div className="relative z-10 max-w-4xl mx-auto flex flex-col items-center text-center space-y-8">
<div className="inline-flex items-center gap-2.5 px-4 py-2 bg-white/10 backdrop-blur-md rounded-full border border-white/20 shadow-inner">
<div className="w-6 h-6 rounded-lg bg-canina-gold text-canina-dark flex items-center justify-center font-black text-xs italic shadow-md">
C
</div>
<span className="text-xs font-black text-white/90 tracking-wider font-vazir">
خبرنامه علمی و تخصصی کنینا آلمان
</span>
<Sparkles className="w-3.5 h-3.5 text-canina-gold animate-pulse" />
</div>
<div className="space-y-4 max-w-2xl">
<h3 className="text-3xl sm:text-4xl font-black font-vazir text-white leading-tight">
مشترک <span className="text-transparent bg-clip-text bg-gradient-to-r from-canina-gold via-white to-canina-teal">دانشنامه پزشکی</span> شوید
</h3>
<p className="text-xs sm:text-sm text-white/80 font-medium font-vazir leading-relaxed">
تازهترین یافتههای علمی تغذیه و سلامت حیوانات خانگی را ماهانه در صندوق ایمیل خود دریافت نمایید.
</p>
</div>
<div className="w-full max-w-xl">
<form
onSubmit={(e) => {
e.preventDefault();
toast.success("عضویت شما در خبرنامه علمی با موفقیت ثبت شد!");
}}
className="w-full bg-white/15 backdrop-blur-xl border border-white/25 p-2 rounded-2xl sm:rounded-full flex flex-col sm:flex-row items-stretch sm:items-center gap-2 sm:gap-3 shadow-2xl hover:border-canina-gold/40 transition-all group"
dir="ltr"
>
<input
type="email"
required
placeholder="آدرس ایمیل شما (name@domain.com)..."
className="flex-1 bg-transparent border-none text-xs sm:text-sm outline-none text-white placeholder:text-white/45 px-3 sm:px-4 py-2.5 sm:py-0 font-sans text-left"
/>
<button
type="submit"
className="bg-canina-gold hover:bg-white text-canina-dark px-6 sm:px-8 py-3.5 rounded-xl sm:rounded-full font-black text-xs sm:text-sm transition-all duration-300 whitespace-nowrap shadow-xl hover:scale-105 flex items-center justify-center gap-2 font-vazir cursor-pointer flex-shrink-0"
dir="rtl"
>
<span>عضویت آنی</span>
<Mail className="w-4 h-4" />
</button>
</form>
</div>
</div>
</motion.div>
</div>
</div>
);
}