canina/frontend/application/components/BlogPostClient.tsx
parsaaghayi bd49fbc4a0
Some checks failed
Deploy Canina / deploy (push) Successful in 2m25s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 7s
fix(security): remediate audit vulnerabilities SEC-001 through SEC-023
2026-09-23 23:00:25 +03:30

421 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, useEffect } from "react";
import DOMPurify from 'dompurify';
import {
ChevronRight,
Calendar,
User,
Clock,
Tag,
Share2,
MessageSquare,
Sparkles,
ShoppingBag,
ArrowLeft,
Send,
Eye,
CheckCircle2,
} from "lucide-react";
import Link from 'next/link';
import SafeImage from "./SafeImage";
import api from "../lib/services/api";
interface BlogPostClientProps {
blog: any;
publishedDate: string;
authorName: string;
}
export default function BlogPostClient({ blog, publishedDate, authorName }: BlogPostClientProps) {
const [copied, setCopied] = useState(false);
const [viewCount, setViewCount] = useState<number>(blog.viewCount || 0);
const [commentName, setCommentName] = useState("");
const [commentEmail, setCommentEmail] = useState("");
const [commentText, setCommentText] = useState("");
const [commentSent, setCommentSent] = useState(false);
const [isSubmittingComment, setIsSubmittingComment] = useState(false);
const [commentError, setCommentError] = useState("");
const relatedProducts = Array.isArray(blog.relatedProducts) ? blog.relatedProducts : [];
const relatedBlogs = Array.isArray(blog.relatedBlogs) ? blog.relatedBlogs : [];
const comments = Array.isArray(blog.comments) ? blog.comments : [];
useEffect(() => {
if (!blog?.slug) return;
const viewKey = `viewed_blog_${blog.slug}`;
const hasViewed = sessionStorage.getItem(viewKey);
api.post(`/blogs/${encodeURIComponent(blog.slug)}/view`)
.then((res) => {
if (res.data?.viewCount !== undefined) {
setViewCount(res.data.viewCount);
} else {
setViewCount((prev) => prev + (hasViewed ? 0 : 1));
}
sessionStorage.setItem(viewKey, '1');
})
.catch((err) => {
console.error('Failed to register view count:', err);
});
}, [blog?.slug]);
const handleShare = () => {
if (typeof window !== 'undefined') {
if (navigator.share) {
navigator.share({ title: blog.title, url: window.location.href }).catch(() => null);
} else {
navigator.clipboard.writeText(window.location.href);
setCopied(true);
setTimeout(() => setCopied(false), 3000);
}
}
};
const handleCommentSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!commentText.trim() || !commentName.trim()) return;
try {
setIsSubmittingComment(true);
setCommentError("");
await api.post(`/blogs/${encodeURIComponent(blog.slug || blog.id)}/comments`, {
userName: commentName.trim(),
email: commentEmail.trim() || undefined,
content: commentText.trim(),
});
setCommentSent(true);
setCommentName("");
setCommentEmail("");
setCommentText("");
} catch (err: any) {
console.error('Failed to submit comment:', err);
setCommentError(err.response?.data?.message || "خطا در ارسال دیدگاه. لطفاً دوباره تلاش کنید.");
} finally {
setIsSubmittingComment(false);
}
};
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">
{/* Breadcrumbs */}
<div className="flex items-center gap-2 text-xs font-bold text-medical-gray-400 mb-8 pb-4 border-b border-medical-gray-200/60 overflow-x-auto">
<Link href="/" className="hover:text-canina-blue transition-colors whitespace-nowrap">خانه</Link>
<ChevronRight className="w-3 h-3 text-medical-gray-300 shrink-0" />
<Link href="/blog" className="hover:text-canina-blue transition-colors whitespace-nowrap">مجله سلامت</Link>
{blog.category && (
<>
<ChevronRight className="w-3 h-3 text-medical-gray-300 shrink-0" />
<Link href={`/blog?categoryId=${blog.category.id}`} className="hover:text-canina-blue transition-colors whitespace-nowrap">
{blog.category.name}
</Link>
</>
)}
<ChevronRight className="w-3 h-3 text-medical-gray-300 shrink-0" />
<span className="text-canina-blue font-black truncate max-w-xs sm:max-w-md">{blog.title}</span>
</div>
<div className="grid lg:grid-cols-12 gap-8 lg:gap-12">
{/* Main Article Content Column */}
<article className="lg:col-span-8 space-y-8">
<div className="bg-white rounded-[3rem] border border-medical-gray-200 shadow-xl overflow-hidden p-6 sm:p-12">
{/* Featured Image */}
{blog.imageUrl && (
<div className="aspect-video w-full overflow-hidden rounded-[2rem] relative mb-8 shadow-sm">
<SafeImage
src={blog.imageUrl}
alt={blog.imageAlt || blog.title}
priority
sizes="(max-width: 1024px) 100vw, 800px"
className="w-full h-full"
imgClassName="w-full h-full object-cover"
/>
{blog.imageCaption && (
<div className="absolute bottom-0 inset-x-0 bg-black/60 backdrop-blur-sm text-white text-xs font-medium py-2 px-4 text-center">
{blog.imageCaption}
</div>
)}
</div>
)}
{/* Meta details bar */}
<div className="flex flex-wrap items-center gap-3 text-xs font-bold text-medical-gray-400 mb-6">
{blog.category && (
<span className="bg-canina-blue/10 text-canina-blue px-3.5 py-1 rounded-full uppercase tracking-wider font-black">
{blog.category.name}
</span>
)}
{publishedDate && (
<div className="flex items-center gap-1">
<Calendar className="w-3.5 h-3.5 text-canina-blue" />
<span>{publishedDate}</span>
</div>
)}
<div className="flex items-center gap-1">
<Clock className="w-3.5 h-3.5 text-canina-blue" />
<span>{blog.readingTime || 4} دقیقه مطالعه</span>
</div>
<div className="flex items-center gap-1">
<Eye className="w-3.5 h-3.5 text-canina-blue" />
<span>{Number(viewCount).toLocaleString('fa-IR')} بازدید</span>
</div>
</div>
{/* Title */}
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-black text-medical-gray-900 leading-tight mb-6">
{blog.title}
</h1>
{/* Excerpt / Summary Callout */}
{blog.excerpt && (
<div className="bg-canina-blue/5 border-r-4 border-canina-blue p-5 rounded-l-2xl mb-8 text-sm sm:text-base font-bold text-medical-gray-800 leading-relaxed italic">
{blog.excerpt}
</div>
)}
{/* Main Rich Content HTML Renderer */}
<div
className="prose prose-base sm:prose-lg max-w-none text-medical-gray-700 leading-loose space-y-5 article-html-content"
dangerouslySetInnerHTML={{
__html: typeof window !== 'undefined'
? DOMPurify.sanitize(blog.content || '', {
ADD_TAGS: ['iframe'],
ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling'],
})
: blog.content || '',
}}
/>
{/* Tags Chip Footer */}
{Array.isArray(blog.tags) && blog.tags.length > 0 && (
<div className="pt-8 mt-10 border-t border-medical-gray-100 flex flex-wrap items-center gap-2">
<span className="text-xs font-bold text-medical-gray-400 flex items-center gap-1">
<Tag className="w-3.5 h-3.5" />
برچسبهای مرتبط:
</span>
{blog.tags.map((tag: any, idx: number) => {
const tagName = typeof tag === 'string' ? tag : tag?.name || '';
const tagId = tag?.id || idx;
if (!tagName) return null;
return (
<Link
key={tagId}
href={`/blog?tag=${encodeURIComponent(tagName)}`}
className="px-3 py-1 bg-purple-50 text-purple-700 hover:bg-purple-100 border border-purple-200 rounded-xl text-xs font-bold transition-colors"
>
#{tagName}
</Link>
);
})}
</div>
)}
{/* Author Box */}
<div className="mt-10 pt-8 border-t border-medical-gray-100 flex flex-col sm:flex-row sm:items-center justify-between gap-4 bg-medical-gray-50/70 p-6 rounded-3xl">
<div className="flex items-center gap-4">
<div className="w-14 h-14 bg-canina-blue/10 rounded-2xl flex items-center justify-center text-canina-blue shadow-inner">
<User className="w-7 h-7" />
</div>
<div>
<div className="font-black text-medical-gray-900 text-base">{authorName}</div>
<div className="font-bold text-xs text-medical-gray-400 mt-0.5">
تیم تحقیق و توسعه علمی کنینا آلمان (Canina Pharma)
</div>
</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={handleShare}
className="p-3 bg-white rounded-2xl hover:bg-canina-blue hover:text-white transition-all text-medical-gray-600 border border-medical-gray-200 shadow-xs cursor-pointer flex items-center gap-1.5 text-xs font-bold"
>
<Share2 className="w-4 h-4" />
<span>{copied ? "لینک کپی شد!" : "اشتراک‌گذاری"}</span>
</button>
</div>
</div>
</div>
{/* Comments Section */}
{blog.allowComments !== false && (
<div className="bg-white rounded-[3rem] border border-medical-gray-200 shadow-xl p-6 sm:p-12 space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-xl font-black text-medical-gray-900 flex items-center gap-2">
<MessageSquare className="w-5 h-5 text-canina-blue" />
<span>دیدگاهها و پرسشهای کاربران ({comments.length})</span>
</h3>
</div>
{commentSent ? (
<div className="p-5 rounded-2xl bg-emerald-50 border border-emerald-200 text-emerald-800 text-xs font-bold leading-relaxed">
دیدگاه شما با موفقیت ثبت شد و پس از بررسی و تایید تیم علمی، در سایت نمایش داده میشود.
</div>
) : (
<form
onSubmit={handleCommentSubmit}
className="space-y-4 bg-medical-gray-50 p-6 rounded-3xl border border-medical-gray-100"
>
<span className="text-xs font-bold text-medical-gray-700 block">
پرسش یا تجربه خود درباره این موضوع را با ما در میان بگذارید:
</span>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<input
type="text"
required
value={commentName}
onChange={(e) => setCommentName(e.target.value)}
placeholder="نام و نام خانوادگی..."
className="px-4 py-2.5 bg-white rounded-xl border border-medical-gray-200 text-xs font-bold outline-none"
/>
<input
type="email"
required
value={commentEmail}
onChange={(e) => setCommentEmail(e.target.value)}
placeholder="آدرس ایمیل..."
className="px-4 py-2.5 bg-white rounded-xl border border-medical-gray-200 text-xs font-bold outline-none"
/>
</div>
<textarea
rows={3}
required
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
placeholder="متن دیدگاه شما..."
className="w-full px-4 py-2.5 bg-white rounded-xl border border-medical-gray-200 text-xs font-bold outline-none"
/>
<button
type="submit"
className="px-6 py-2.5 bg-canina-blue text-white rounded-xl text-xs font-black hover:bg-canina-blue/90 transition-all flex items-center gap-1.5 cursor-pointer shadow-md"
>
<Send className="w-3.5 h-3.5" />
<span>ارسال نظر</span>
</button>
</form>
)}
{/* Comments List */}
{comments.length > 0 ? (
<div className="space-y-3 pt-4">
{comments.map((c: any) => (
<div key={c.id} className="p-4 rounded-2xl bg-medical-gray-50 border border-medical-gray-100 space-y-1">
<div className="flex items-center justify-between text-xs font-bold">
<span className="text-medical-gray-900">{c.authorName}</span>
<span className="text-medical-gray-400 text-[10px]">
{c.createdAt ? new Date(c.createdAt).toLocaleDateString('fa-IR') : ''}
</span>
</div>
<p className="text-xs text-medical-gray-600 leading-relaxed">{c.content}</p>
</div>
))}
</div>
) : (
<p className="text-xs text-medical-gray-400 text-center py-4">
هنوز دیدگاهی برای این مقاله ثبت نشده است. اولین نظر را شما بنویسید!
</p>
)}
</div>
)}
</article>
{/* Sidebar Column */}
<aside className="lg:col-span-4 space-y-8">
{/* Related Products Box */}
{relatedProducts.length > 0 && (
<div className="bg-canina-blue rounded-[3rem] p-8 text-white shadow-2xl relative overflow-hidden">
<div className="absolute top-0 left-0 p-8 opacity-10 pointer-events-none">
<Sparkles className="w-24 h-24" />
</div>
<div className="flex items-center gap-2 mb-6 relative z-10">
<ShoppingBag className="w-5 h-5 text-canina-gold" />
<h4 className="text-xl font-black italic text-white">مکملهای مرتبط این مقاله</h4>
</div>
<div className="space-y-4 relative z-10">
{relatedProducts.map((p: any) => (
<Link
key={p.id}
href={`/shop/${p.slug || p.id}`}
className="bg-white/10 hover:bg-white/20 border border-white/20 p-3.5 rounded-2xl flex items-center gap-3 transition-all group"
>
<div className="w-14 h-14 bg-white rounded-xl p-1.5 shrink-0 overflow-hidden relative group-hover:scale-105 transition-transform">
<SafeImage
src={p.imageUrl || (Array.isArray(p.images) && p.images[0]) || '/assets/images/canina-product-placeholder.png'}
alt={p.nameFa}
className="w-full h-full"
imgClassName="w-full h-full object-contain"
/>
</div>
<div className="flex-1 min-w-0">
<span className="text-[10px] font-bold text-white/60 block truncate">{p.category?.name || p.categorySlug}</span>
<h5 className="text-xs font-black text-white truncate group-hover:text-canina-gold transition-colors">
{p.nameFa}
</h5>
{(p.priceDisplay || p.priceValue || p.prices?.[0]?.price) && (
<span className="text-[11px] font-bold text-canina-gold font-mono mt-0.5 block">
{p.priceDisplay || (p.priceValue ? `${Number(p.priceValue).toLocaleString('fa-IR')} تومان` : `${Number(p.prices[0].price).toLocaleString('fa-IR')} تومان`)}
</span>
)}
</div>
</Link>
))}
</div>
</div>
)}
{/* Related Blogs Box */}
{relatedBlogs.length > 0 && (
<div className="bg-white rounded-[3rem] border border-medical-gray-200 p-8 shadow-xl space-y-6">
<h4 className="text-lg font-black text-medical-gray-900">مقالات پیشنهادی</h4>
<div className="space-y-4">
{relatedBlogs.map((item: any) => (
<Link
key={item.id}
href={`/blog/${encodeURIComponent(item.slug || item.id)}`}
className="flex items-center gap-3 group"
>
<div className="w-16 h-14 rounded-2xl overflow-hidden shrink-0 relative bg-medical-gray-100">
<SafeImage
src={item.imageUrl || '/assets/images/canina-product-placeholder.png'}
alt={item.title}
className="w-full h-full"
imgClassName="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
/>
</div>
<div className="flex-1 min-w-0">
<h5 className="text-xs font-bold text-medical-gray-900 group-hover:text-canina-blue transition-colors line-clamp-2 leading-snug">
{item.title}
</h5>
<span className="text-[10px] text-medical-gray-400 mt-1 block">
{item.readingTime || 3} دقیقه مطالعه
</span>
</div>
</Link>
))}
</div>
</div>
)}
{/* Scientific Consultation CTA */}
<div className="bg-gradient-to-br from-medical-gray-900 to-canina-blue rounded-[3rem] p-8 text-white shadow-xl space-y-4">
<div className="inline-flex p-3 rounded-2xl bg-white/10">
<Sparkles className="w-6 h-6 text-canina-gold" />
</div>
<h4 className="text-lg font-black leading-snug">نیاز به راهنمایی و دوز درمانی دارید؟</h4>
<p className="text-xs text-white/80 leading-relaxed">
مشاوران علمی و دامپزشکان داروسازی کنینا آماده پاسخگویی به سوالات شما در مورد وضعیت سلامت پت شما هستند.
</p>
<Link
href="/smart-advisor"
className="inline-flex items-center justify-center gap-2 w-full py-3 rounded-2xl bg-canina-gold text-canina-dark text-xs font-black hover:bg-white transition-all shadow-lg"
>
<span>مشاوره هوشمند دارویی</span>
<ArrowLeft className="w-4 h-4" />
</Link>
</div>
</aside>
</div>
</div>
</div>
);
}