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 && ( +
+
+
+
+
+
+
+
+
+
+
+
+
+ )} +
+ + {/* عکس پروفایل */} +
+
+ {author.name +
+
+ 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 && ( + + Telegram + + )} + {author.socials?.whatsapp && ( + + WhatsApp + + )} + {author.socials?.email && ( + + 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.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..fc854589 --- /dev/null +++ b/src/app/[lang]/news/categories/[category]/[slug]/components/NewsContent.tsx @@ -0,0 +1,194 @@ +// 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('