From 3933dbba654c8907d87b1f34c6162755bcd127cf Mon Sep 17 00:00:00 2001
From: GitHub Actions
Date: Thu, 7 May 2026 23:48:13 +0330
Subject: [PATCH 1/2] add single news page
---
next.config.js | 44 +-
.../[slug]/components/AuthorCard.tsx | 96 +++
.../[slug]/components/AuthorSection.tsx | 175 ++++++
.../[slug]/components/NewsContent.tsx | 195 +++++++
.../[slug]/components/NewsGallery.tsx | 45 ++
.../[slug]/components/NewsHeader.tsx | 15 +
.../[slug]/components/NewsImage.tsx | 143 +++++
.../[slug]/components/NewsNavCard.tsx | 80 +++
.../[slug]/components/NewsSideCard.tsx | 149 +++++
.../[slug]/components/NewsStats.tsx | 134 +++++
.../[category]/[slug]/components/NewsTags.tsx | 117 ++++
.../[slug]/components/PrevNextNews.tsx | 152 +++++
.../components/RelatedArticlesSlider.tsx | 168 ++++++
.../[slug]/components/ShowSocialWrapper.tsx | 74 +++
.../[slug]/components/ShredpageArticle.tsx | 180 ++++++
.../[category]/[slug]/components/SideCard.tsx | 117 ++++
.../categories/[category]/[slug]/page.tsx | 545 ++++++++++++++++++
.../news/categories/[category]/page.tsx | 299 ++++++----
src/app/[lang]/news/categories/page.tsx | 145 ++++-
.../[lang]/news/components/ArticleCard.tsx | 1 +
src/app/[lang]/news/components/LatestNews.tsx | 2 +-
.../[lang]/news/components/MainNewsCard.tsx | 1 +
.../[lang]/news/components/PopularNews.tsx | 28 +-
.../[lang]/news/components/VideoNewsList.tsx | 109 ++--
src/app/[lang]/news/page.tsx | 225 ++++++--
src/components/utils/news.json | 122 ++++
src/styles/global.css | 12 +
27 files changed, 3130 insertions(+), 243 deletions(-)
create mode 100644 src/app/[lang]/news/categories/[category]/[slug]/components/AuthorCard.tsx
create mode 100644 src/app/[lang]/news/categories/[category]/[slug]/components/AuthorSection.tsx
create mode 100644 src/app/[lang]/news/categories/[category]/[slug]/components/NewsContent.tsx
create mode 100644 src/app/[lang]/news/categories/[category]/[slug]/components/NewsGallery.tsx
create mode 100644 src/app/[lang]/news/categories/[category]/[slug]/components/NewsHeader.tsx
create mode 100644 src/app/[lang]/news/categories/[category]/[slug]/components/NewsImage.tsx
create mode 100644 src/app/[lang]/news/categories/[category]/[slug]/components/NewsNavCard.tsx
create mode 100644 src/app/[lang]/news/categories/[category]/[slug]/components/NewsSideCard.tsx
create mode 100644 src/app/[lang]/news/categories/[category]/[slug]/components/NewsStats.tsx
create mode 100644 src/app/[lang]/news/categories/[category]/[slug]/components/NewsTags.tsx
create mode 100644 src/app/[lang]/news/categories/[category]/[slug]/components/PrevNextNews.tsx
create mode 100644 src/app/[lang]/news/categories/[category]/[slug]/components/RelatedArticlesSlider.tsx
create mode 100644 src/app/[lang]/news/categories/[category]/[slug]/components/ShowSocialWrapper.tsx
create mode 100644 src/app/[lang]/news/categories/[category]/[slug]/components/ShredpageArticle.tsx
create mode 100644 src/app/[lang]/news/categories/[category]/[slug]/components/SideCard.tsx
create mode 100644 src/app/[lang]/news/categories/[category]/[slug]/page.tsx
create mode 100644 src/components/utils/news.json
diff --git a/next.config.js b/next.config.js
index 801b39bc..09d842d1 100644
--- a/next.config.js
+++ b/next.config.js
@@ -7,7 +7,7 @@ const nextConfig = {
// 🔹 فعالکردن سورسمپ در پروداکشن (برای رفع هشدار Missing source maps)
productionBrowserSourceMaps: true,
-logging: {
+ logging: {
fetches: {
fullUrl: true,
},
@@ -68,26 +68,30 @@ logging: {
return config;
},
-images: {
- deviceSizes: [320, 480, 640, 768, 1024, 1280, 1536],
- imageSizes: [16, 32, 64, 128, 256, 384, 512, 540, 600],
+ images: {
+ deviceSizes: [320, 480, 640, 768, 1024, 1280, 1536],
+ imageSizes: [16, 32, 64, 128, 256, 384, 512, 540, 600],
+qualities: [25, 50, 75],
+ formats: ['image/avif', 'image/webp'],
+ remotePatterns: [
+ { protocol: 'https', hostname: 'dl.qzparadise.ir' },
+ { protocol: 'https', hostname: 'api.metarang.com' },
+ { protocol: 'https', hostname: 'api.rgb.irpsc.com' },
+ { protocol: 'https', hostname: 'admin.metarang.com' },
+ { protocol: 'https', hostname: 'admin.rgb.irpsc.com' },
+ { protocol: 'https', hostname: '*.irpsc.com' },
+ { protocol: 'https', hostname: 'rgb.irpsc.com' },
+ { protocol: 'http', hostname: 'rgb.irpsc.com' },
+ { protocol: 'https', hostname: 'metarang.com' },
+ { protocol: 'http', hostname: 'localhost' },
+ { protocol: 'https', hostname: 'irpsc.com' },
+ { protocol: 'https', hostname: 'frdevelop2.irpsc.com' },
+ { protocol: 'https', hostname: 'supabase.com' },
+ { protocol: 'https', hostname: '3d.irpsc.com' },
+ { protocol: 'https', hostname: 'metarang.com' },
- formats: ['image/avif', 'image/webp'],
- remotePatterns: [
- { protocol: 'https', hostname: 'dl.qzparadise.ir' },
- { protocol: 'https', hostname: 'api.metarang.com' },
- { protocol: 'https', hostname: 'api.rgb.irpsc.com' },
- { protocol: 'https', hostname: 'admin.metarang.com' },
- { protocol: 'https', hostname: 'admin.rgb.irpsc.com' },
- { protocol: 'http', hostname: 'localhost' },
- { protocol: 'https', hostname: 'irpsc.com' },
- { protocol: 'https', hostname: 'frdevelop2.irpsc.com' },
- { protocol: 'https', hostname: 'supabase.com' },
- { protocol: 'https', hostname: '3d.irpsc.com' },
- { protocol: 'https', hostname: 'metarang.com' },
-
- ],
-},
+ ],
+ },
};
diff --git a/src/app/[lang]/news/categories/[category]/[slug]/components/AuthorCard.tsx b/src/app/[lang]/news/categories/[category]/[slug]/components/AuthorCard.tsx
new file mode 100644
index 00000000..2d7c5113
--- /dev/null
+++ b/src/app/[lang]/news/categories/[category]/[slug]/components/AuthorCard.tsx
@@ -0,0 +1,96 @@
+"use client";
+
+import React, { useState } from "react";
+import Image from "next/image";
+import Link from "next/link";
+import { findByUniqueId } from "@/components/utils/findByUniqueId";
+interface AuthorCardProps {
+ lang: string;
+ article: any; // میتونی تایپ دقیق هم بزنی
+ mainData: { mainData: string }
+}
+
+const AuthorCard = ({ lang, article, mainData }: AuthorCardProps) => {
+ const author = article?.author;
+ if (!author) return null;
+ const [linkLoading, setLinkLoading] = useState(false);
+ return (
+
+ {linkLoading && (
+
+ )}
+
+
+ {/* عکس پروفایل */}
+
+
+
+
+
+ setLinkLoading(true)} href={`/${lang}/citizens/${author.citizenId.toLowerCase()}` || ""} className="text-sm text-blue-500 mt-2">{author.citizenId}
+
{author.name}
+
+
+
+ {/* حوزه فعالیت و شبکهها */}
+
+
+
+ {findByUniqueId(mainData, 1508)} {author.field}
+
+
+
+ {author.socials?.telegram && (
+
+
+
+ )}
+ {author.socials?.whatsapp && (
+
+
+
+ )}
+ {author.socials?.email && (
+
+
+
+ )}
+
+
+
+ {/* بیوگرافی */}
+
+ {author.bio}
+
+
+ {/* دکمه دیدن مقالات نویسنده */}
+
setLinkLoading(true)}
+ href={`/${lang}/citizens/${author.citizenId.toLowerCase()}` || ""}
+ className="mt-6 px-5 py-2 rounded-lg bg-light-primary dark:bg-dark-yellow dark:text-black text-white font-bold text-sm hover:opacity-90 transition u"
+ >
+ {findByUniqueId(mainData, 1512)}
+
+
+
+ );
+};
+
+export default AuthorCard;
diff --git a/src/app/[lang]/news/categories/[category]/[slug]/components/AuthorSection.tsx b/src/app/[lang]/news/categories/[category]/[slug]/components/AuthorSection.tsx
new file mode 100644
index 00000000..29e1f547
--- /dev/null
+++ b/src/app/[lang]/news/categories/[category]/[slug]/components/AuthorSection.tsx
@@ -0,0 +1,175 @@
+import React from "react";
+import Image from "next/image";
+import Link from "next/link";
+import { Like, Dislike, View } from "@/components/svgs/SvgEducation";
+import { findByUniqueId } from "@/components/utils/findByUniqueId";
+
+interface NewsMetaProps {
+ author: {
+ name: string;
+ citizenId: string;
+ avatar: string;
+ bio?: string;
+ field?: string;
+ } | string; // میتواند string (JSON) یا object باشد
+ stats: {
+ views: number;
+ likes?: number;
+ dislikes?: number;
+ comments?: number;
+ };
+ // date?: string | null;
+ title: string;
+ excerpt?: string;
+ content?: any;
+ mainData: { mainData: string };
+ lang: string;
+}
+
+// تابع کمکی برای تبدیل author به object
+function parseAuthor(author: NewsMetaProps['author']): {
+ name: string;
+ citizenId: string;
+ avatar: string;
+ bio?: string;
+ field?: string;
+} {
+ // اگر已經是 object
+ if (typeof author === 'object' && author !== null && !Array.isArray(author)) {
+ return {
+ name: author.name || "نویسنده",
+ citizenId: author.citizenId || "",
+ avatar: author.avatar || "/clogo.png",
+ bio: author.bio,
+ field: author.field,
+ };
+ }
+
+ // اگر string است، try to parse as JSON
+ if (typeof author === 'string') {
+ try {
+ const parsed = JSON.parse(author);
+ return {
+ name: parsed.name || "نویسنده",
+ citizenId: parsed.citizenId || "",
+ avatar: parsed.avatar || "/clogo.png",
+ bio: parsed.bio,
+ field: parsed.field,
+ };
+ } catch (e) {
+ console.error("Failed to parse author JSON:", e);
+ }
+ }
+
+ // fallback
+ return {
+ name: "نویسنده",
+ citizenId: "",
+ avatar: "/clogo.png",
+ };
+}
+
+export default function NewsMeta({
+ author,
+ // date,
+ // excerpt,
+ title,
+ content,
+ stats,
+ mainData,
+ lang
+}: NewsMetaProps) {
+ // تبدیل author به object معتبر
+ const parsedAuthor = parseAuthor(author);
+
+ // پاک کردن HTML و محاسبه تعداد کلمات
+ const plainText = content
+ ? content
+ .replace(/<[^>]+>/g, " ") // حذف تگهای HTML
+ .replace(/\s+/g, " ") // همه فاصلهها و خطوط جدید را به یک فاصله تبدیل کن
+ .trim()
+ : "";
+
+ const words = plainText ? plainText.split(" ").length : 0;
+ const readingTime = Math.max(1, Math.ceil(words / 200)); // 200 کلمه در دقیقه
+
+ return (
+
+
+
+
+
+
+
+
+
+ {parsedAuthor.name}
+
+ {parsedAuthor.citizenId && (
+
+ {parsedAuthor.citizenId}
+
+ )}
+
+
+
+
+
+
+ {stats?.views ?? 0}
+
+
+
+
+
+
+ {findByUniqueId(mainData, 1503) || "زمان مطالعه"}: {readingTime} {findByUniqueId(mainData, 33) || "دقیقه"}
+
+
+
+
+
+
+
{title}
+ {/* optionally uncomment excerpt */}
+ {/* {excerpt && (
+
+ {excerpt}
+
+ )} */}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/[lang]/news/categories/[category]/[slug]/components/NewsContent.tsx b/src/app/[lang]/news/categories/[category]/[slug]/components/NewsContent.tsx
new file mode 100644
index 00000000..6ac92282
--- /dev/null
+++ b/src/app/[lang]/news/categories/[category]/[slug]/components/NewsContent.tsx
@@ -0,0 +1,195 @@
+// components/news/NewsContent.tsx
+
+"use client";
+
+import React, { useMemo, useState, useEffect } from "react";
+import NewsGallerySimple from "./NewsGallery";
+
+type NewsContentProps = {
+ content: string;
+ gallery?: string[] | null;
+ mainImage?: string;
+ galleryTitle?: string;
+};
+
+// کامپوننت اسکلت لودینگ گالری
+// کامپوننت اسکلت لودینگ گالری با افکت shimmer
+const GallerySkeleton = ({ title }: { title?: string }) => {
+ return (
+
+
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+
+ );
+};
+
+export default function NewsContent({
+ content,
+ gallery,
+ mainImage,
+ galleryTitle = "گالری تصاویر"
+}: NewsContentProps) {
+ const [isGalleryLoading, setIsGalleryLoading] = useState(true);
+ const [showRealGallery, setShowRealGallery] = useState(false);
+
+ // شبیهسازی لودینگ گالری
+ useEffect(() => {
+ if (!gallery || gallery.length === 0) {
+ setIsGalleryLoading(false);
+ return;
+ }
+
+ // تاخیر برای نمایش اسکلت لودینگ
+ const timer = setTimeout(() => {
+ setIsGalleryLoading(false);
+ setShowRealGallery(true);
+ }, 500);
+
+ return () => clearTimeout(timer);
+ }, [gallery]);
+
+ // تابع برای پیدا کردن موقعیت مناسب بعد از پاراگراف یا لیست
+ const findInsertPosition = (htmlContent: string): number => {
+ // پیدا کردن تمام تگهای بسته شدن پاراگراف و لیست
+ const closingTags: { position: number; type: string }[] = [];
+ let searchIndex = 0;
+
+ // پیدا کردن پاراگرافها
+ while (true) {
+ const pClose = htmlContent.indexOf('
', searchIndex);
+ if (pClose === -1) break;
+ closingTags.push({ position: pClose + 4, type: 'p' });
+ searchIndex = pClose + 4;
+ }
+
+ // پیدا کردن لیستهای نامرتب (ul)
+ searchIndex = 0;
+ while (true) {
+ const ulClose = htmlContent.indexOf('', searchIndex);
+ if (ulClose === -1) break;
+ closingTags.push({ position: ulClose + 5, type: 'ul' });
+ searchIndex = ulClose + 5;
+ }
+
+ // پیدا کردن لیستهای مرتب (ol)
+ searchIndex = 0;
+ while (true) {
+ const olClose = htmlContent.indexOf('', searchIndex);
+ if (olClose === -1) break;
+ closingTags.push({ position: olClose + 5, type: 'ol' });
+ searchIndex = olClose + 5;
+ }
+
+ // مرتبسازی بر اساس موقعیت
+ closingTags.sort((a, b) => a.position - b.position);
+
+ // پیدا کردن سومین عنصر (پاراگراف یا لیست)
+ if (closingTags.length >= 3) {
+ const thirdElement = closingTags[2];
+ let insertPos = thirdElement.position;
+
+ // چک کردن اینکه بعد از سومین عنصر، لیستی وجود دارد یا نه
+ const remainingContent = htmlContent.slice(insertPos);
+ const nextListIndex = Math.min(
+ remainingContent.indexOf('') !== -1 ? remainingContent.indexOf('') : Infinity,
+ remainingContent.indexOf('') !== -1 ? remainingContent.indexOf('') : Infinity
+ );
+
+ const nextPIndex = remainingContent.indexOf('');
+
+ // اگر بعد از عنصر سوم، اولین چیزی که میآید لیست باشد
+ if (nextListIndex !== Infinity && (nextListIndex < nextPIndex || nextPIndex === -1)) {
+ // پیدا کردن انتهای آن لیست
+ const listTag = remainingContent[nextListIndex] === '<' && remainingContent[nextListIndex + 1] === 'u' ? 'ul' : 'ol';
+ const listCloseTag = `${listTag}>`;
+ const listEndIndex = remainingContent.indexOf(listCloseTag);
+
+ if (listEndIndex !== -1) {
+ insertPos = insertPos + listEndIndex + listCloseTag.length;
+ }
+ }
+
+ return insertPos;
+ }
+
+ // اگر کمتر از 3 عنصر داشت، بعد از آخرین عنصر قرار بده
+ if (closingTags.length > 0) {
+ return closingTags[closingTags.length - 1].position;
+ }
+
+ return -1;
+ };
+
+ // تابع برای تقسیم محتوا
+ const renderContentWithGallery = useMemo(() => {
+ if (!gallery || gallery.length === 0) {
+ return { before: content, after: null, showGallery: false };
+ }
+
+ const insertPosition = findInsertPosition(content);
+
+ if (insertPosition !== -1 && insertPosition < content.length) {
+ return {
+ before: content.slice(0, insertPosition),
+ after: content.slice(insertPosition),
+ showGallery: true
+ };
+ }
+
+ // اگر موقعیت مناسبی پیدا نشد، گالری را به انتها اضافه کن
+ return { before: content, after: null, showGallery: true };
+ }, [content, gallery]);
+
+ // اگر گالری وجود ندارد
+ if (!renderContentWithGallery.showGallery || !gallery || gallery.length === 0) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* بخش اول محتوا (قبل از گالری) */}
+
+
+ {/* اسکلت لودینگ گالری */}
+ {isGalleryLoading && (
+
+ )}
+
+ {/* گالری واقعی */}
+ {!isGalleryLoading && showRealGallery && (
+
+ )}
+
+ {/* بخش دوم محتوا (بعد از گالری) */}
+ {renderContentWithGallery.after && (
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/[lang]/news/categories/[category]/[slug]/components/NewsGallery.tsx b/src/app/[lang]/news/categories/[category]/[slug]/components/NewsGallery.tsx
new file mode 100644
index 00000000..71312782
--- /dev/null
+++ b/src/app/[lang]/news/categories/[category]/[slug]/components/NewsGallery.tsx
@@ -0,0 +1,45 @@
+// components/shared/NewsGallerySimple.tsx
+
+"use client";
+
+import React, { useState } from "react";
+import Image from "next/image";
+
+interface NewsGallerySimpleProps {
+ gallery?: string[] | null;
+ mainImage?: string;
+}
+
+export default function NewsGallerySimple({ gallery, mainImage }: NewsGallerySimpleProps) {
+ if (!gallery || gallery.length === 0) {
+ return null;
+ }
+
+ const allImages = mainImage && !gallery.includes(mainImage)
+ ? [mainImage, ...gallery]
+ : gallery;
+
+ return (
+
+
+
+
+ {allImages.map((image, index) => (
+
+
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/[lang]/news/categories/[category]/[slug]/components/NewsHeader.tsx b/src/app/[lang]/news/categories/[category]/[slug]/components/NewsHeader.tsx
new file mode 100644
index 00000000..f6a9768f
--- /dev/null
+++ b/src/app/[lang]/news/categories/[category]/[slug]/components/NewsHeader.tsx
@@ -0,0 +1,15 @@
+type NewsHeaderProps = {
+ title:any;
+ author:any;
+ description: any ;
+ date:any;
+};
+
+export default function NewsHeader({ description }: NewsHeaderProps) {
+ return (
+
+ );
+}
diff --git a/src/app/[lang]/news/categories/[category]/[slug]/components/NewsImage.tsx b/src/app/[lang]/news/categories/[category]/[slug]/components/NewsImage.tsx
new file mode 100644
index 00000000..6fc4495e
--- /dev/null
+++ b/src/app/[lang]/news/categories/[category]/[slug]/components/NewsImage.tsx
@@ -0,0 +1,143 @@
+"use client";
+
+import React, { useRef, useState } from "react";
+import Image from "next/image";
+import Link from "next/link";
+
+interface News {
+ image?: string | null;
+ title: string;
+ video?: string | null;
+ slug?: string;
+ categorySlug?: string;
+}
+
+interface Params {
+ lang: string;
+ category?: string;
+}
+
+interface NewsImageProps {
+ news: News;
+ params?: Params;
+ mainData?: { mainData: string };
+}
+
+const NewsImage: React.FC = ({ news, params, mainData }) => {
+ const [showVideo, setShowVideo] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const [videoError, setVideoError] = useState(false);
+ const videoRef = useRef(null);
+
+ const hasVideo = news.video && news.video !== "" && news.video !== null;
+
+ const handlePlayVideo = () => {
+ if (!hasVideo) return;
+ setIsLoading(true);
+ setShowVideo(true);
+ setVideoError(false);
+ };
+
+ const handleVideoCanPlay = () => {
+ setIsLoading(false);
+ videoRef.current?.play().catch((err) => {
+ console.error("Video play failed:", err);
+ setIsLoading(false);
+ setVideoError(true);
+ });
+ };
+
+ const handleVideoError = () => {
+ console.error("Video error:", news.video);
+ setIsLoading(false);
+ setVideoError(true);
+ setShowVideo(false);
+ };
+
+ // اگر ویدیو خطا داشته باشد یا وجود نداشته باشد، فقط عکس نشان بده
+ if (!hasVideo || videoError) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ {/* نمایش عکس (قبل از کلیک روی پلی) */}
+ {!showVideo && (
+ <>
+
+
+ {/* دکمه پلی روی عکس */}
+
+ >
+ )}
+
+ {/* نمایش ویدیو (بعد از کلیک روی پلی) */}
+ {showVideo && (
+
+ {isLoading && (
+
+ )}
+
+
+ )}
+
+
+ );
+};
+
+export default NewsImage;
\ No newline at end of file
diff --git a/src/app/[lang]/news/categories/[category]/[slug]/components/NewsNavCard.tsx b/src/app/[lang]/news/categories/[category]/[slug]/components/NewsNavCard.tsx
new file mode 100644
index 00000000..170cad0f
--- /dev/null
+++ b/src/app/[lang]/news/categories/[category]/[slug]/components/NewsNavCard.tsx
@@ -0,0 +1,80 @@
+"use client";
+
+import Link from "next/link";
+import Image from "next/image";
+import { Like, Dislike, View } from "@/components/svgs/SvgEducation";
+import NewsMeta from "./AuthorSection";
+
+interface NewsNavCardProps {
+ href: string;
+ news: any;
+ onClickCapture?: () => void;
+ activeLoadingId?: any;
+ setActiveLoadingId?: any;
+}
+
+const ArticleNavCard = ({ href, news, activeLoadingId, setActiveLoadingId}: NewsNavCardProps) => {
+ const isLoading = activeLoadingId === news.id;
+ return (
+
+
setActiveLoadingId(news.id)}
+ href={href}
+
+ className={`${isLoading ? "rotating-border-card cursor-not-allowed" : ""} flex flex-col gap-1 bg-white dark:bg-[#1A1A18] shadow-md rounded-2xl overflow-hidden w-full h-[390px]`}
+ >
+ {isLoading && (
+
+ {/* بکگراند محو */}
+
+
+ )}
+ {/* IMAGE */}
+
+
+ {/* CONTENT */}
+
+
+
تاریخ انتشار: {news.date}
+
+
+
+
+ {news.stats?.views ?? 0}
+
+
+
+ {news.stats?.likes ?? 0}
+
+
+
+ {news.stats?.dislikes ?? 0}
+
+
+
+
+
+
+ {news.title}
+
+
+ {news.excerpt}
+
+
+
+
+
+ );
+};
+
+export default ArticleNavCard;
diff --git a/src/app/[lang]/news/categories/[category]/[slug]/components/NewsSideCard.tsx b/src/app/[lang]/news/categories/[category]/[slug]/components/NewsSideCard.tsx
new file mode 100644
index 00000000..108f85dd
--- /dev/null
+++ b/src/app/[lang]/news/categories/[category]/[slug]/components/NewsSideCard.tsx
@@ -0,0 +1,149 @@
+"use client";
+
+import Link from "next/link";
+import Image from "next/image";
+import { View, Like, Dislike } from "@/components/svgs/SvgEducation";
+
+interface NewsSideCardProps {
+ news: any;
+ href: string;
+ mainData?: any;
+ activeLoadingId?: any;
+ setActiveLoadingId?: any;
+ isLoading?: boolean;
+}
+
+// کامپوننت اسکلت لودینگ با افکت shimmer
+const SideCardSkeleton: React.FC = () => {
+ return (
+
+ {/* Shimmer effect overlay */}
+
+
+
+
+
+
+
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+};
+
+const NewsSideCard: React.FC = ({
+ news,
+ href,
+ mainData,
+ activeLoadingId,
+ setActiveLoadingId,
+ isLoading
+}) => {
+ // اگر در حالت اسکلت لودینگ هستیم
+ if (isLoading || !news) {
+ return ;
+ }
+
+ const cleanDescription = (html: string, limit = 255): string => {
+ if (!html) return "";
+
+ let text = "";
+
+ if (typeof window === "undefined") {
+ text = html.replace(/<|>/g, " ");
+ } else {
+ const div = document.createElement("div");
+ div.innerHTML = html;
+ text = div.textContent || div.innerText || "";
+ }
+
+ text = text.replace(/\s+/g, " ").trim();
+
+ return text.length > limit
+ ? text.slice(0, limit).trim() + "…"
+ : text;
+ };
+
+ const isLoadingState = activeLoadingId === news?.id;
+
+ return (
+ setActiveLoadingId?.(news?.id)}
+ className={`${isLoadingState ? "rotating-border-card cursor-not-allowed" : ""} bg-white dark:bg-[#1A1A18] shadow-lg rounded-xl overflow-hidden w-full flex flex-col hover:scale-[1.02] transition-transform relative`}
+ >
+ {isLoadingState && (
+
+ )}
+
+
+
+
+
+ {mainData && {mainData} : }
+ {news?.date}
+
+
+
+
+ {news?.stats?.views ?? 0}
+
+
+
+ {news?.stats?.likes ?? 0}
+
+
+
+ {news?.stats?.dislikes ?? 0}
+
+
+
+
+
+
{news?.title}
+
{cleanDescription(news?.description)}
+
+
+ );
+};
+
+export default NewsSideCard;
\ No newline at end of file
diff --git a/src/app/[lang]/news/categories/[category]/[slug]/components/NewsStats.tsx b/src/app/[lang]/news/categories/[category]/[slug]/components/NewsStats.tsx
new file mode 100644
index 00000000..0b6d8fdb
--- /dev/null
+++ b/src/app/[lang]/news/categories/[category]/[slug]/components/NewsStats.tsx
@@ -0,0 +1,134 @@
+
+import { Like, Dislike, View, Comment } from "@/components/svgs/SvgEducation";
+import { findByUniqueId } from "@/components/utils/findByUniqueId";
+import Link from "next/link";
+interface NewsStatsProps {
+ stats: {
+ views?: number;
+ likes?: number;
+ dislikes?: number;
+ comments?: number;
+ };
+ category?: string | null;
+ categorySlug?: string | null;
+ date?: string | null;
+ readingTime?: string | number | null;
+ mainData: any;
+ lang?: string;
+ className?: string;
+ showIcons?: boolean;
+}
+
+export default function NewsStats({
+ stats,
+ category,
+categorySlug,
+ date,
+ readingTime,
+ mainData,
+ lang,
+ className = "",
+ showIcons = true,
+}: NewsStatsProps) {
+ // مقداردهی پیشفرض
+ const views = stats?.views ?? 0;
+ const likes = stats?.likes ?? 0;
+ const dislikes = stats?.dislikes ?? 0;
+ const comments = stats?.comments ?? 0;
+
+ // فرمت کردن تاریخ شمسی
+
+
+ return (
+
+ {/* دستهبندی */}
+ {category && (
+
+
+ {category}
+
+
+ )}
+
+ {/* تاریخ */}
+ {date && (
+
+ {showIcons && (
+
+ )}
+
{date}
+
+ )}
+
+ {/* زمان مطالعه */}
+ {readingTime && (
+
+ {showIcons && (
+
+ )}
+
+ {findByUniqueId(mainData, 1503) || "زمان مطالعه"}: {readingTime}{" "}
+ {findByUniqueId(mainData, 33) || "دقیقه"}
+
+
+ )}
+
+ {/* جداساز */}
+
+
+ {/* بازدیدها */}
+
+ {showIcons && }
+ {views.toLocaleString("fa-IR")}
+
+
+ {/* لایکها */}
+ {likes >= 0 && (
+
+ {showIcons && }
+ {likes.toLocaleString("fa-IR")}
+
+ )}
+
+ {/* دیسلایکها */}
+ {dislikes >= 0 && (
+
+ {showIcons && }
+ {dislikes.toLocaleString("fa-IR")}
+
+ )}
+
+ {/* کامنتها */}
+ {comments >= 0 && (
+
+ {showIcons && }
+ {comments.toLocaleString("fa-IR")}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/[lang]/news/categories/[category]/[slug]/components/NewsTags.tsx b/src/app/[lang]/news/categories/[category]/[slug]/components/NewsTags.tsx
new file mode 100644
index 00000000..6e0fda5c
--- /dev/null
+++ b/src/app/[lang]/news/categories/[category]/[slug]/components/NewsTags.tsx
@@ -0,0 +1,117 @@
+// components/shared/ArticleTagsSimple.tsx
+
+import React from "react";
+
+type Tag = {
+ label: string;
+ slug?: string;
+};
+
+interface ArticleTagsSimpleProps {
+ tags?: Tag[] | string[] | string | null;
+ className?: string;
+ showIcon?: boolean;
+ limit?: number;
+}
+
+export default function ArticleTagsSimple({
+ tags,
+ className = "",
+ showIcon = true,
+ limit,
+}: ArticleTagsSimpleProps) {
+ // تبدیل tags به آرایه استاندارد از stringها
+ const normalizeTags = (): string[] => {
+ if (!tags) return [];
+
+ // اگر already آرایه است
+ if (Array.isArray(tags)) {
+ return tags
+ .map((tag) => {
+ if (typeof tag === "string") {
+ return tag;
+ }
+ if (typeof tag === "object" && tag !== null && "label" in tag) {
+ return tag.label;
+ }
+ return null;
+ })
+ .filter((tag): tag is string => tag !== null);
+ }
+
+ // اگر string JSON است
+ if (typeof tags === "string") {
+ try {
+ const parsed = JSON.parse(tags);
+ if (Array.isArray(parsed)) {
+ return parsed
+ .map((tag) => {
+ if (typeof tag === "string") {
+ return tag;
+ }
+ if (typeof tag === "object" && tag !== null) {
+ return tag.label || tag.slug || String(tag);
+ }
+ return null;
+ })
+ .filter((tag): tag is string => tag !== null);
+ }
+ } catch (e) {
+ console.error("Failed to parse tags JSON:", e);
+ }
+ }
+
+ return [];
+ };
+
+ const safeTags = normalizeTags();
+ const displayTags = limit ? safeTags.slice(0, limit) : safeTags;
+
+ if (safeTags.length === 0) {
+ return null;
+ }
+
+ return (
+
+
تگ ها
+
+ {displayTags.map((tag, index) => (
+
+ {showIcon && (
+
+ )}
+
+ {tag}
+
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/[lang]/news/categories/[category]/[slug]/components/PrevNextNews.tsx b/src/app/[lang]/news/categories/[category]/[slug]/components/PrevNextNews.tsx
new file mode 100644
index 00000000..baf90f89
--- /dev/null
+++ b/src/app/[lang]/news/categories/[category]/[slug]/components/PrevNextNews.tsx
@@ -0,0 +1,152 @@
+"use client";
+
+import React, { useEffect, useState } from "react";
+import Link from "next/link";
+import Image from "next/image";
+import { supabase } from "@/utils/lib/supabaseClient";
+import { Like, Dislike, View } from "@/components/svgs/SvgEducation";
+import { findByUniqueId } from "@/components/utils/findByUniqueId";
+import NewsNavCard from "./NewsNavCard";
+
+// ایمپورت دیتای استاتیک به عنوان fallback
+import fallbackNewsData from "@/components/utils/news.json";
+
+interface PrevNextNewsProps {
+ params: {
+ lang: string;
+ slug: string;
+ category: string;
+ };
+ news?: any[]; // اختیاری میکنیم چون میتواند از fallback بیاید
+ mainData: { mainData: string };
+}
+
+const PrevNextNews = ({ params, news: propNews, mainData }: PrevNextNewsProps) => {
+ const [news, setNews] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [activeLoadingId, setActiveLoadingId] = useState(null);
+ const [usingFallback, setUsingFallback] = useState(false);
+
+ // ===============================
+ // 📌 گرفتن همه اخبار (با fallback)
+ // ===============================
+ useEffect(() => {
+ const fetchNews = async () => {
+ // اگر news از طریق props آمده، از همان استفاده کن
+ if (propNews && propNews.length > 0) {
+ setNews(propNews);
+ setLoading(false);
+ return;
+ }
+
+ // تلاش برای دریافت از Supabase
+ try {
+ const { data, error } = await supabase
+ .from("news")
+ .select("*")
+ .order("date", { ascending: true });
+
+ if (!error && data && data.length > 0) {
+ setNews(data);
+ setUsingFallback(false);
+ } else {
+ // استفاده از fallback
+ console.warn("⚠️ PrevNextNews: Using fallback news.json");
+ const fallbackData = [...fallbackNewsData].sort((a, b) => {
+ const dateA = a.date ? parseInt(a.date.replace(/\//g, "")) : 0;
+ const dateB = b.date ? parseInt(b.date.replace(/\//g, "")) : 0;
+ return dateA - dateB;
+ });
+ setNews(fallbackData);
+ setUsingFallback(true);
+ }
+ } catch (err) {
+ // در صورت خطا، از fallback استفاده کن
+ console.error("❌ PrevNextNews: Error fetching from Supabase, using fallback", err);
+ const fallbackData = [...fallbackNewsData].sort((a, b) => {
+ const dateA = a.date ? parseInt(a.date.replace(/\//g, "")) : 0;
+ const dateB = b.date ? parseInt(b.date.replace(/\//g, "")) : 0;
+ return dateA - dateB;
+ });
+ setNews(fallbackData);
+ setUsingFallback(true);
+ }
+
+ setLoading(false);
+ };
+
+ fetchNews();
+ }, [propNews]);
+
+ if (loading) return null;
+
+ // مقاله فعلی
+ const currentIndex = news.findIndex((a) => a.slug === params.slug);
+ if (currentIndex === -1) return null;
+
+ const prevNews = currentIndex > 0 ? news[currentIndex - 1] : null;
+ const nextNews = currentIndex < news.length - 1 ? news[currentIndex + 1] : null;
+
+ // اگر هیچ مقاله قبلی و بعدی نبود
+ if (!prevNews && !nextNews) return null;
+
+ return (
+
+
+
+
+ {/* ======================= */}
+ {/* 📌 کارت مقاله قبلی */}
+ {/* ======================= */}
+
+ {prevNews ? (
+
+ {findByUniqueId(mainData, 1506) || "مطلب قبلی"}
+
+ ) : (
)}
+
+ {prevNews ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+ {/* ======================= */}
+ {/* 📌 کارت مقاله بعدی */}
+ {/* ======================= */}
+
+
+
+ {findByUniqueId(mainData, 1507) || "مطلب بعدی"}
+
+
+
+ {nextNews ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+ );
+};
+
+export default PrevNextNews;
\ No newline at end of file
diff --git a/src/app/[lang]/news/categories/[category]/[slug]/components/RelatedArticlesSlider.tsx b/src/app/[lang]/news/categories/[category]/[slug]/components/RelatedArticlesSlider.tsx
new file mode 100644
index 00000000..dd8c9856
--- /dev/null
+++ b/src/app/[lang]/news/categories/[category]/[slug]/components/RelatedArticlesSlider.tsx
@@ -0,0 +1,168 @@
+"use client";
+
+import React, { useState, useEffect, useRef } from "react";
+import { Swiper, SwiperSlide } from "swiper/react";
+import type { Swiper as SwiperType } from "swiper";
+import "swiper/css";
+import Link from "next/link";
+import { ArrowRight } from "@/components/svgs";
+import { findByUniqueId } from "@/components/utils/findByUniqueId";
+import ArticleCard from "../../../../components/ArticleCard";
+import { supabase } from "@/utils/lib/supabaseClient";
+
+interface RelatedArticlesSliderProps {
+ params: { lang: string; slug: string };
+ mainData: any;
+}
+
+const RelatedArticlesSlider = ({ params, mainData }: RelatedArticlesSliderProps) => {
+ const [relatedArticles, setRelatedArticles] = useState([]);
+ const [currentArticle, setCurrentArticle] = useState(null);
+ const [activeIndex, setActiveIndex] = useState(0);
+ const [activeLoadingId, setActiveLoadingId] = useState(null);
+ const swiperRef = useRef();
+
+ // === 1) دریافت مقاله فعلی با slug ===
+ useEffect(() => {
+ const fetchCurrent = async () => {
+ const { data, error } = await supabase
+ .from("articles")
+ .select("*")
+ .eq("slug", params.slug)
+ .single();
+
+ if (!error && data) {
+ setCurrentArticle(data);
+ }
+ };
+
+ fetchCurrent();
+ }, [params.slug]);
+
+ // === 2) دریافت مقالات مرتبط پس از لود شدن مقاله اصلی ===
+ useEffect(() => {
+ if (!currentArticle) return;
+
+ const fetchRelated = async () => {
+ const { data, error } = await supabase
+ .from("articles")
+ .select("*")
+ .neq("slug", params.slug); // حذف مقاله فعلی
+
+ if (!error && data) {
+ const filtered = data
+ .filter(
+ (a) =>
+ a.category === currentArticle.category ||
+ a.subCategory === currentArticle.subCategory
+ )
+ .slice(0, 10);
+
+ setRelatedArticles(filtered);
+ }
+ };
+
+ fetchRelated();
+ }, [currentArticle]);
+
+ if (!currentArticle) return null;
+ if (relatedArticles.length === 0) return null;
+
+ return (
+
+ {/* Header */}
+
+
{findByUniqueId(mainData, 1511)}
+
+
+ {findByUniqueId(mainData, 171)}
+
+
+
+
+
+ {/* Slider */}
+ (swiperRef.current = swiper)}
+ breakpoints={{
+ 0: { slidesPerView: 1.2, spaceBetween: 20 },
+ 640: { slidesPerView: 2, spaceBetween: 20 },
+ 1024: { slidesPerView: 3.5, spaceBetween: 20 },
+ }}
+ onSlideChange={(swiper) => setActiveIndex(swiper.realIndex)}
+ >
+ {relatedArticles.map((item) => (
+
+
+
+ ))}
+
+
+ {/* Controls */}
+
+
+ {/* Prev */}
+
+
+ {/* Pagination */}
+
+ {relatedArticles.map((_, idx) => (
+
+
+ {/* Next */}
+
+
+
+
+ );
+};
+
+export default RelatedArticlesSlider;
diff --git a/src/app/[lang]/news/categories/[category]/[slug]/components/ShowSocialWrapper.tsx b/src/app/[lang]/news/categories/[category]/[slug]/components/ShowSocialWrapper.tsx
new file mode 100644
index 00000000..c0408e25
--- /dev/null
+++ b/src/app/[lang]/news/categories/[category]/[slug]/components/ShowSocialWrapper.tsx
@@ -0,0 +1,74 @@
+"use client";
+
+import { useState } from "react";
+import ShredPageArticle from "./ShredpageArticle";
+import { findByUniqueId } from "@/components/utils/findByUniqueId";
+import { CopyIcon } from "@/components/svgs/SvgCategories";
+import { Like, Dislike, Comment } from "@/components/svgs/SvgEducation";
+
+interface NewsStats {
+ comments: number;
+ likes: number;
+ dislikes: number;
+
+}
+
+interface News {
+ stats: NewsStats;
+ slug: string; // اضافه شد
+ categorySlug?: string; // اضافه شد
+}
+
+export default function ShowSocialWrapper({
+ params,
+ mainData,
+ news,
+}: {
+ params: any;
+ mainData: any;
+ news: any;
+}) {
+ const [showSocial, setShowSocial] = useState(false);
+
+ return (
+
+
+
setShowSocial(!showSocial)}
+ className="dark:bg-dark-yellow bg-blueLink flex flex-row w-max items-center gap-2 cursor-pointer rounded-[10px] 3xl:py-[3px] 3xl:px-4 lg:py-2 lg:px-2 md:py-2 md:px-4 sm:py-2 sm:px-4 xs:py-1 xs:px-2"
+ >
+
+ {findByUniqueId(mainData, 244)}
+
+
+
+
+
+
+
+
+ {news.stats.comments}
+
+
+
+ {news.stats.likes}
+
+
+
+ {news.stats.dislikes}
+
+
+
+
+ {/* مودال یا بخش اشتراکگذاری */}
+ {showSocial && (
+
+ )}
+
+ );
+}
diff --git a/src/app/[lang]/news/categories/[category]/[slug]/components/ShredpageArticle.tsx b/src/app/[lang]/news/categories/[category]/[slug]/components/ShredpageArticle.tsx
new file mode 100644
index 00000000..de239bf4
--- /dev/null
+++ b/src/app/[lang]/news/categories/[category]/[slug]/components/ShredpageArticle.tsx
@@ -0,0 +1,180 @@
+"use client";
+
+import { useState, useRef } from "react";
+import Image from "next/image";
+import { CLoseIcon } from "@/svgs/index";
+import { Arrow } from "@/svgs/SvgEducation";
+
+// ANIMATION
+import { motion } from "framer-motion";
+import { Tooltip as ReactTooltip } from "react-tooltip";
+import { findByUniqueId } from "@/components/utils/findByUniqueId";
+
+interface ShareArticlePageProps {
+ setShowSocial: (show: boolean) => void;
+ mainData: any;
+ params: { lang: string; slug: string };
+ article: { slug: string; categorySlug?: string }; // اضافه شده
+}
+
+export default function ShareArticlePage({
+ setShowSocial,
+ mainData,
+ params,
+ article,
+}: ShareArticlePageProps) {
+ const [copied, setCopied] = useState(false);
+ const scrollContainer = useRef(null);
+
+ // شبکههای اجتماعی
+ const items = [
+ { id: 1, img: "/shared/whatsapp.png", title: "WhatsApp" },
+ { id: 2, img: "/shared/telegram.png", title: "Telegram" },
+ { id: 3, img: "/shared/facebook.png", title: "Facebook" },
+ { id: 4, img: "/shared/twitter.png", title: "Twitter" },
+ { id: 5, img: "/shared/linkedin.png", title: "Linkedin" },
+ ];
+
+ // لینک کامل با categorySlug
+const categorySlug = article?.categorySlug ?? "all"; // fallback امن
+const fullUrl = `https://metarang.com/${params.lang}/news/categories/${categorySlug}/${params.slug}`;
+
+
+ // کپی لینک
+ const handleCopyClick = async () => {
+ try {
+ await navigator.clipboard.writeText(fullUrl);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1000);
+ } catch (error) {
+ console.error("Error copying URL:", error);
+ }
+ };
+
+ // اشتراکگذاری روی شبکهها
+ const handleShare = (platform: string) => {
+ let shareUrl = "";
+ switch (platform) {
+ case "WhatsApp":
+ shareUrl = `https://api.whatsapp.com/send?text=${encodeURIComponent(fullUrl)}`;
+ break;
+ case "Telegram":
+ shareUrl = `https://t.me/share/url?url=${encodeURIComponent(fullUrl)}`;
+ break;
+ case "Facebook":
+ shareUrl = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(fullUrl)}`;
+ break;
+ case "Twitter":
+ shareUrl = `https://twitter.com/intent/tweet?url=${encodeURIComponent(fullUrl)}&text=YourTextHere`;
+ break;
+ case "Linkedin":
+ shareUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(fullUrl)}`;
+ break;
+ }
+ window.open(shareUrl, "_blank");
+ };
+
+ const scrollRight = () => {
+ if (scrollContainer.current) {
+ scrollContainer.current.scrollBy({ left: 100, behavior: "smooth" });
+ }
+ };
+
+ const scrollLeft = () => {
+ if (scrollContainer.current) {
+ scrollContainer.current.scrollBy({ left: -100, behavior: "smooth" });
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ {/* Close Button */}
+
setShowSocial(false)}
+ alt="Close"
+ />
+
+ {/* Title */}
+
+ {findByUniqueId(mainData, 324)}
+
+
+ {/* arrows */}
+
+
+
+ {/* icons */}
+
+
+ {items.map((item) => (
+
handleShare(item.title)}
+ >
+
+
{item.title}
+
+ ))}
+
+
+
+ {/* copy link */}
+
+
+ {findByUniqueId(mainData, 323)}
+
+
{fullUrl}
+
+
+ {copied && (
+
+ {params.lang === "fa" ? "کپی شد!" : "Copied!"}
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/src/app/[lang]/news/categories/[category]/[slug]/components/SideCard.tsx b/src/app/[lang]/news/categories/[category]/[slug]/components/SideCard.tsx
new file mode 100644
index 00000000..9ab717a4
--- /dev/null
+++ b/src/app/[lang]/news/categories/[category]/[slug]/components/SideCard.tsx
@@ -0,0 +1,117 @@
+"use client";
+
+import React, { useEffect, useState } from "react";
+import Image from "next/image";
+import Link from "next/link";
+import { supabase } from "@/utils/lib/supabaseClient";
+import { View, Like, Dislike } from "@/components/svgs/SvgEducation";
+import { ArrowRight } from "@/components/svgs";
+import { findByUniqueId } from "@/components/utils/findByUniqueId";
+import NewsSideCard from "./NewsSideCard";
+
+// ایمپورت دیتای استاتیک به عنوان fallback
+import fallbackNewsData from "@/components/utils/news.json";
+
+interface SideCardProps {
+ params: any;
+ mainData: any;
+}
+
+const SideCard: React.FC = ({ params, mainData }) => {
+ const [latestNews, setLatestNews] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [activeLoadingId, setActiveLoadingId] = useState(null);
+
+ useEffect(() => {
+ fetchLatest();
+ }, []);
+
+ const fetchLatest = async () => {
+ // تلاش برای دریافت از Supabase
+ const { data, error } = await supabase
+ .from("news")
+ .select("*")
+ .order("date", { ascending: false })
+ .limit(5);
+
+ // اگر خطایی رخ داد یا دیتایی نیامد، از fallback استفاده کن
+ if (error || !data || data.length === 0) {
+ if (error) {
+ console.warn("⚠️ SideCard: Supabase error, using fallback news.json", error.message);
+ } else if (!data || data.length === 0) {
+ console.warn("⚠️ SideCard: No data from Supabase, using fallback news.json");
+ }
+
+ // استفاده از دیتای fallback
+ const fallbackData = fallbackNewsData
+ .map((item: any) => ({
+ ...item,
+ stats: typeof item.stats === 'string' ? JSON.parse(item.stats) : item.stats,
+ }))
+ .sort((a, b) => {
+ // مرتبسازی بر اساس تاریخ (جدیدترین اول)
+ const dateA = a.date ? parseInt(a.date.replace(/\//g, "")) : 0;
+ const dateB = b.date ? parseInt(b.date.replace(/\//g, "")) : 0;
+ return dateB - dateA;
+ })
+ .slice(0, 5);
+
+ setLatestNews(fallbackData);
+ setLoading(false);
+ return;
+ }
+
+ setLatestNews(data);
+ setLoading(false);
+ };
+
+ if (loading) {
+ return (
+
+
+ {[1, 2, 3, 4, 5].map((i) => (
+
+ ))}
+
+ );
+ }
+
+ return (
+
+
+
{findByUniqueId(mainData, 1504)}
+
+
+
+ {findByUniqueId(mainData, 171)}
+
+
+
+
+
+ {latestNews.map((item) => (
+
+ ))}
+
+ );
+};
+
+export default SideCard;
\ No newline at end of file
diff --git a/src/app/[lang]/news/categories/[category]/[slug]/page.tsx b/src/app/[lang]/news/categories/[category]/[slug]/page.tsx
new file mode 100644
index 00000000..9a7adfe5
--- /dev/null
+++ b/src/app/[lang]/news/categories/[category]/[slug]/page.tsx
@@ -0,0 +1,545 @@
+export const dynamic = "force-dynamic";
+
+import NotFoundPage from "@/components/shared/NotFoundPage";
+import { supabase } from "@/utils/lib/supabaseClient";
+
+import {
+ getTranslation,
+ getMainFile,
+ getLangArray,
+} from "@/components/utils/actions";
+import BreadCrumb from "@/components/shared/BreadCrumb";
+import { findByUniqueId } from "@/components/utils/findByUniqueId";
+import AuthorSection from "./components/AuthorSection";
+import NewsHeader from "./components/NewsHeader";
+import NewsImage from "./components/NewsImage";
+import NewsContent from "./components/NewsContent";
+import SideCard from "./components/SideCard";
+import PopularNews from "../../../components/PopularNews";
+
+import NewsStats from "./components/NewsStats";
+import PrevNextNews from "./components/PrevNextNews";
+import ShowSocialWrapper from "./components/ShowSocialWrapper";
+import CustomErrorPage from "@/components/shared/CustomErrorPage";
+import CleanAutoRetryParam from "@/components/shared/CleanAutoRetryParam";
+import NewsTags from "./components/NewsTags";
+import fallbackNewsData from "@/components/utils/news.json";
+
+interface NewsPageProps {
+ params: Promise<{
+ lang: string;
+ category: string;
+ slug: string;
+ }>;
+}
+
+// ─── تابع کمکی برای نرمالایز کردن دیتای دریافتی ───
+type NormalizedNews = {
+ id: number;
+ title: string;
+ slug: string;
+ image?: string | null;
+ date?: string | null;
+ readingTime?: string | null;
+ stats?: any;
+ category?: string | null;
+ categorySlug?: string | null;
+ categoryImage?: string | null;
+ categoryDec?: string | null;
+ subCategory?: string | null;
+ content: string;
+ description?: string | null;
+ author?: any;
+ tags?: any;
+ video?: string | null;
+ excerpt?: string | null;
+ gallery?: string | null;
+};
+
+function normalizeNewsItem(item: any): NormalizedNews {
+ return {
+ id: item.id,
+ title: item.title,
+ slug: item.slug,
+ image: item.image || null,
+ date: item.date || null,
+ readingTime: item.readingTime || null,
+ stats: item.stats || null,
+ category: item.category || null,
+ categorySlug: item.categorySlug || null,
+ categoryImage: item.categoryImage || null,
+ categoryDec: item.categoryDec || null,
+ subCategory: item.subCategory || null,
+ content: item.content,
+ description: item.description || null,
+ author: item.author || null,
+ tags: item.tags || null,
+ video: item.video || null,
+ excerpt: item.excerpt || null,
+ gallery: item.gallery || null,
+ };
+}
+
+// ─── تابع دریافت خبر با fallback به news.json ───
+async function fetchNewsBySlugWithFallback(slug: string) {
+ try {
+ // تلاش برای دریافت از Supabase
+ const { data: supabaseNews, error } = await supabase
+ .from("news")
+ .select("*")
+ .eq("slug", slug)
+ .single();
+
+ // اگر خطایی رخ داد یا دیتایی نیامد، از fallback استفاده کن
+ if (error || !supabaseNews) {
+ if (error) {
+ console.warn(`⚠️ [News:${slug}] Supabase error, using fallback news.json:`, error.message);
+ } else if (!supabaseNews) {
+ console.warn(`⚠️ [News:${slug}] No data from Supabase, using fallback news.json`);
+ }
+
+ // جستجو در دیتای news.json بر اساس slug
+ const fallbackItem = fallbackNewsData.find((item: any) => item.slug === slug);
+
+ if (fallbackItem) {
+ return { data: normalizeNewsItem(fallbackItem), fromFallback: true };
+ }
+
+ return { data: null, fromFallback: true };
+ }
+
+ // نرمالایز کردن دیتای دریافتی از Supabase
+ const normalizedData = normalizeNewsItem(supabaseNews);
+
+ return { data: normalizedData, fromFallback: false };
+ } catch (err) {
+ // در صورت بروز هرگونه خطای غیرمنتظره، از fallback استفاده کن
+ console.error(`❌ [News:${slug}] Unexpected error fetching news, using fallback:`, err);
+ const fallbackItem = fallbackNewsData.find((item: any) => item.slug === slug);
+
+ if (fallbackItem) {
+ return { data: normalizeNewsItem(fallbackItem), fromFallback: true };
+ }
+
+ return { data: null, fromFallback: true };
+ }
+}
+
+// ─── تابع دریافت اخبار همان دسته برای Prev/Next با fallback ───
+async function fetchCategoryNewsWithFallback(categorySlug: string, currentSlug: string) {
+ try {
+ // تلاش برای دریافت از Supabase
+ const { data: supabaseNews, error } = await supabase
+ .from("news")
+ .select("*")
+ .eq("categorySlug", categorySlug)
+ .order("date", { ascending: true });
+
+ // اگر خطایی رخ داد یا دیتایی نیامد، از fallback استفاده کن
+ if (error || !supabaseNews || supabaseNews.length === 0) {
+ if (error) {
+ console.warn(`⚠️ [Category:${categorySlug}] Supabase error for Prev/Next, using fallback`);
+ }
+
+ // دریافت از fallback و فیلتر بر اساس categorySlug
+ const fallbackData = fallbackNewsData
+ .map((item: any) => normalizeNewsItem(item))
+ .filter((item: NormalizedNews) => item.categorySlug === categorySlug)
+ .sort((a: NormalizedNews, b: NormalizedNews) => {
+ // مرتبسازی ساده بر اساس تاریخ (فرمت شمسی)
+ const dateA = a.date ? parseInt(a.date.replace(/\//g, "")) : 0;
+ const dateB = b.date ? parseInt(b.date.replace(/\//g, "")) : 0;
+ return dateA - dateB;
+ });
+
+ return { data: fallbackData, fromFallback: true };
+ }
+
+ // نرمالایز کردن دیتای دریافتی از Supabase
+ const normalizedData = supabaseNews.map((item: any) => normalizeNewsItem(item));
+
+ return { data: normalizedData, fromFallback: false };
+ } catch (err) {
+ console.error(`❌ [Category:${categorySlug}] Error fetching category news for Prev/Next:`, err);
+ const fallbackData = fallbackNewsData
+ .map((item: any) => normalizeNewsItem(item))
+ .filter((item: NormalizedNews) => item.categorySlug === categorySlug);
+
+ return { data: fallbackData, fromFallback: true };
+ }
+}
+
+// ======================================
+// Metadata (SEO + 404 امن)
+// ======================================
+export async function generateMetadata({ params }: NewsPageProps) {
+ const resolvedParams = await params;
+ const { lang } = resolvedParams;
+ try {
+ function cleanDescription(html: any, limit = 255) {
+ if (!html) return "";
+
+ let text = "";
+
+ if (typeof window === "undefined") {
+ // SSR / Node.js
+ text = html.replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim();
+ } else {
+ // Browser
+ const div = document.createElement("div");
+ div.innerHTML = html;
+ text = div.textContent || div.innerText || "";
+ }
+
+ text = text.trim();
+
+ return text.length > limit
+ ? text.slice(0, limit).trim() + "…"
+ : text;
+ }
+
+ const { slug, category } = resolvedParams;
+
+ // دریافت خبر با fallback
+ const { data: news } = await fetchNewsBySlugWithFallback(slug);
+
+ if (!news) {
+ return {
+ title: "خبر یافت نشد",
+ description: "خبر مورد نظر وجود ندارد",
+ robots: { index: false, follow: false },
+ };
+ }
+
+ if (category !== news.categorySlug) {
+ return {
+ title: "خبر یافت نشد",
+ description: "آدرس خبر معتبر نیست",
+ robots: { index: false, follow: false },
+ };
+ }
+
+ const canonicalUrl = `https://metarang.com/${lang}/news/categories/${news.categorySlug}/${news.slug}`;
+
+ return {
+ title: news.title,
+ description: cleanDescription(news.description || "اخبار متاورس رنگ"),
+ alternates: { canonical: canonicalUrl },
+ openGraph: {
+ title: news.title,
+ description: cleanDescription(news.description || "اخبار متاورس رنگ"),
+ url: canonicalUrl,
+ type: "article",
+ images: news.image ? [{ url: news.image }] : [],
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: news.title,
+ description: cleanDescription(news.description || "اخبار متاورس رنگ"),
+ images: news.image ? [news.image] : [],
+ },
+ robots: { index: true, follow: true },
+ };
+ } catch (error) {
+ console.error("❌ Metadata error (NewsPage):", error);
+
+ return {
+ title: "خطا",
+ description: "مشکلی در بارگذاری صفحه رخ داده است",
+ };
+ }
+}
+
+// ======================================
+// Static Paths - بر اساس دیتای استاتیک news.json
+// ======================================
+export async function generateStaticParams() {
+ try {
+ if (!fallbackNewsData || fallbackNewsData.length === 0) return [];
+
+ // فقط فارسی (طبق دیتای موجود، همه اخبار فارسی هستند)
+ return fallbackNewsData.map((news: any) => ({
+ lang: "fa",
+ category: news.categorySlug,
+ slug: news.slug,
+ }));
+ } catch (error) {
+ console.error("Error generating static params:", error);
+ return [];
+ }
+}
+
+// هر ۱۰ دقیقه یکبار صفحات استاتیک رو بهروزرسانی کنه (ISR)
+
+export const dynamicParams = true; // اجازه میده اخبار جدید بدون rebuild هم کار کنن
+
+// ======================================
+// صفحه اصلی خبر
+// ======================================
+export default async function NewsPage({ params }: NewsPageProps) {
+ const resolvedParams = await params;
+ const { lang } = resolvedParams;
+ try {
+ function cleanDescription(html: any, limit = 100) {
+ if (!html) return "";
+
+ // Normalize to string
+ let text = String(html);
+
+ // Remove any angle brackets to prevent partial tags (e.g., "