canina/frontend/application/components/BlogPreviewSection.tsx
parsa aghaei 0b52f9e9e4
Some checks failed
Deploy Canina / deploy (push) Successful in 1m38s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 6s
style(homepage): optimize mobile margins, paddings and section spacing
2026-08-26 11:09:39 +03:30

180 lines
7.7 KiB
TypeScript

"use client";
import React, { useEffect, useState } from "react";
import Link from "next/link";
import { BookOpen, Calendar, Eye, MessageSquare, ArrowLeft } from "lucide-react";
import { motion } from "motion/react";
import SafeImage from "./SafeImage";
import { useSettingsStore } from "../lib/store/settingsStore";
import api, { BASE_DOMAIN } from "../lib/services/api";
import { toPersian } from "../lib/utils";
export interface BlogPost {
id: string;
title: string;
slug: string;
content: string;
imageUrl?: string;
viewCount: number;
createdAt: string;
author?: {
firstName: string;
lastName: string;
};
_count?: {
comments: number;
};
}
export default function BlogPreviewSection() {
const getText = useSettingsStore((state) => state.getText);
const [blogs, setBlogs] = useState<BlogPost[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function loadBlogs() {
try {
setIsLoading(true);
const res = await api.get("/blogs/homepage");
const list = Array.isArray(res.data?.blogs)
? res.data.blogs
: Array.isArray(res.data)
? res.data
: [];
setBlogs(list);
} catch (err) {
console.error("Failed to load homepage blogs:", err);
setBlogs([]);
} finally {
setIsLoading(false);
}
}
loadBlogs();
}, []);
if (!isLoading && blogs.length === 0) {
return null;
}
return (
<section className="py-8 sm:py-20 bg-white font-vazir" dir="rtl">
<div className="max-w-7xl mx-auto px-4">
{/* Section Header */}
<div className="flex flex-col md:flex-row md:items-end justify-between mb-6 sm:mb-16 gap-4 sm:gap-6">
<div className="max-w-2xl">
<div className="inline-flex items-center gap-2 px-3 py-1 bg-canina-blue/10 text-canina-blue rounded-full text-[10px] font-black uppercase tracking-widest mb-3 sm:mb-4">
<BookOpen className="w-3 h-3" />
<span>{getText("blog_preview_badge", "دانشنامه و مقالات علمی")}</span>
</div>
<h2 className="text-2xl sm:text-4xl md:text-5xl font-black text-medical-gray-900 leading-tight font-lalezar tracking-wide">
{getText("blog_preview_title", "تازه‌ترین مقالات")} <span className="text-canina-blue">{getText("blog_preview_title_highlight", "دامپزشکی و سلامت")}</span>
</h2>
<p className="text-sm sm:text-lg text-medical-gray-500 font-medium mt-2 sm:mt-4">
{getText("blog_preview_desc", "راهنماهای علمی و کاربردی برای نگهداری، تغذیه و پیشگیری از بیماری‌های سگ و گربه بر اساس آخرین تحقیقات بین‌المللی.")}
</p>
</div>
<Link
href="/blog"
className="inline-flex items-center gap-1.5 sm:gap-2 text-canina-blue font-bold text-xs sm:text-sm hover:translate-x-1 transition-transform rtl:hover:-translate-x-1"
>
<span>{getText("blog_preview_view_all", "مشاهده همه مقالات")}</span>
<ArrowLeft className="w-4 h-4" />
</Link>
</div>
{/* Blog Grid */}
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{[1, 2, 3].map((i) => (
<div
key={i}
className="bg-medical-gray-50 rounded-3xl p-4 border border-medical-gray-100 animate-pulse space-y-4"
>
<div className="h-48 bg-medical-gray-200 rounded-2xl" />
<div className="h-6 bg-medical-gray-200 rounded-md w-3/4" />
<div className="h-4 bg-medical-gray-200 rounded-md w-1/2" />
</div>
))}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{blogs.map((blog, idx) => {
const image = blog.imageUrl?.startsWith("http")
? blog.imageUrl
: blog.imageUrl
? `${BASE_DOMAIN}${blog.imageUrl}`
: "/assets/images/blog-placeholder.png";
const authorName = blog.author
? `${blog.author.firstName || ""} ${blog.author.lastName || ""}`.trim()
: "تحریریه کنینا";
const formattedDate = new Date(blog.createdAt).toLocaleDateString("fa-IR");
const blogHref = `/blog/${encodeURIComponent(blog.slug || blog.id)}`;
return (
<motion.article
key={blog.id || idx}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: idx * 0.1 }}
className="bg-medical-gray-50/60 rounded-[2rem] border border-medical-gray-100 overflow-hidden hover:shadow-lg hover:-translate-y-1 transition-all flex flex-col group"
>
<Link href={blogHref} className="block relative aspect-video w-full overflow-hidden bg-medical-gray-100">
<SafeImage
src={image}
alt={blog.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-105 transition-transform duration-500"
/>
</Link>
<div className="p-6 flex-1 flex flex-col justify-between space-y-4">
<div className="space-y-2">
<div className="flex items-center gap-4 text-xs text-medical-gray-400 font-medium">
<span className="flex items-center gap-1">
<Calendar className="w-3.5 h-3.5" />
{toPersian(formattedDate)}
</span>
<span className="flex items-center gap-1">
<Eye className="w-3.5 h-3.5" />
{toPersian(blog.viewCount || 0)} {getText("blog_preview_views", "بازدید")}
</span>
<span className="flex items-center gap-1">
<MessageSquare className="w-3.5 h-3.5" />
{toPersian(blog._count?.comments || 0)} {getText("blog_preview_comments", "نظر")}
</span>
</div>
<Link href={blogHref}>
<h3 className="text-lg font-black text-medical-gray-900 group-hover:text-canina-blue transition-colors line-clamp-2 leading-snug">
{blog.title}
</h3>
</Link>
</div>
<div className="pt-4 border-t border-medical-gray-200/60 flex items-center justify-between">
<span className="text-xs font-bold text-medical-gray-500">
{authorName}
</span>
<Link
href={blogHref}
className="text-xs font-black text-canina-blue group-hover:underline flex items-center gap-1"
>
{getText("blog_preview_read_article", "خواندن مقاله")}
<ArrowLeft className="w-3 h-3" />
</Link>
</div>
</div>
</motion.article>
);
})}
</div>
)}
</div>
</section>
);
}