Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 24 additions & 20 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const nextConfig = {
// 🔹 فعال‌کردن سورس‌مپ در پروداکشن (برای رفع هشدار Missing source maps)
productionBrowserSourceMaps: true,

logging: {
logging: {
fetches: {
fullUrl: true,
},
Expand Down Expand Up @@ -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' },

],
},
],
},

};

Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<section className="w-full flex justify-center my-10">
{linkLoading && (
<div className="fixed top-0 left-0 bottom-0 w-full h-screen z-[70] flex items-center justify-center bg-black/60 backdrop-blur-sm" >
<div className="container flex w-full h-screen items-center justify-center md:ms-[25vw] lg:ms-[17vw] xl:ms-[15vw] 3xl:ms-[16vw]">
<div className="holder">
<div className="box"></div>
</div>
<div className="holder">
<div className="box"></div>
</div>
<div className="holder">
<div className="box"></div>
</div>
</div>
</div>
)}
<div className="bg-white dark:bg-[#1A1A18] border border-gray-200 dark:border-gray-700 rounded-2xl shadow-md p-6 w-full flex flex-col items-center text-center md:px-12">

{/* عکس پروفایل */}
<div className="flex flex-col gap-4 mt-[-85px]">
<div className="relative w-[120px] h-[120px] bg-lightGray rounded-full overflow-hidden border shadow-md">
<Image
src={author.avatar || "/articles/author/fallback-avatar.jpg"}
alt={author.name || "نویسنده"}
width={120}
height={120}
className="object-cover"
/>
</div>
<div className="flex flex-col text-center">
<Link onClickCapture={() => setLinkLoading(true)} href={`/${lang}/citizens/${author.citizenId.toLowerCase()}` || ""} className="text-sm text-blue-500 mt-2">{author.citizenId}</Link>
<h2 className="text-lg font-bold mt-1 dark:text-white">{author.name}</h2>
</div>
</div>

{/* حوزه فعالیت و شبکه‌ها */}
<div className="flex flex-col lg:flex-row md:flex-row md:justify-between w-full gap-5 items-center mt-5 md:mt-[-34px]">
<div>
<p className="text-sm text-gray-500 dark:text-dark-gray">
{findByUniqueId(mainData, 1508)} {author.field}
</p>
</div>
<div className="flex items-center gap-4">
{author.socials?.telegram && (
<a href={author.socials.telegram} target="_blank" rel="noopener noreferrer">
<Image width={37} height={37} src="/social/telegram-circle.png" alt="Telegram" />
</a>
)}
{author.socials?.whatsapp && (
<a href={`https://wa.me/${author.socials.whatsapp}`} target="_blank" rel="noopener noreferrer">
<Image width={37} height={37} src="/social/whatsapp-circle.png" alt="WhatsApp" />
</a>
)}
{author.socials?.email && (
<a href={`mailto:${author.socials.email}`}>
<Image width={37} height={37} src="/social/envelope-circle.png" alt="Email" />
</a>
)}
</div>
</div>

{/* بیوگرافی */}
<p className="mt-5 text-sm text-gray-600 dark:text-dark-gray leading-relaxed max-w-2xl">
{author.bio}
</p>

{/* دکمه دیدن مقالات نویسنده */}
<Link onClickCapture={() => 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)}
</Link>
</div>
</section>
);
};

export default AuthorCard;
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col gap-5 w-full">
<div className="flex justify-between w-full">
<div className="flex items-center gap-2 text-sm text-gray-600">
<div>
<Image
src={parsedAuthor.avatar || "/clogo.png"}
alt={parsedAuthor.name}
width={60}
height={60}
className="rounded-full aspect-square w-[50px] h-[50px] md:w-[80px] md:h-[80px]"
/>
</div>

<div className="flex flex-col gap-2 justify-between">
<span className="text-xs md:text-xl dark:text-white">
{parsedAuthor.name}
</span>
{parsedAuthor.citizenId && (
<Link
href={`/${lang}/citizens/${parsedAuthor.citizenId}`}
className="text-xs md:text-base text-blueLink dark:text-blue-500 uppercase"
>
{parsedAuthor.citizenId}
</Link>
)}
</div>
</div>

<div className="flex flex-col text-start justify-center">
<span className="items-center gap-1 flex text-[10px] md:text-sm text-[#868B90] text-start md:hidden">
<View className="stroke-textGray dark:stroke-[#888888] size-[16px]" />
{stats?.views ?? 0}
</span>
<span className="text-[10px] md:text-sm text-[#868B90] text-start flex items-center gap-1">
<span className="dark:text-white text-base mt-[4px] ms-[-1px]">
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
className="dark:stroke-[#888888] stroke-2"
d="M15.5635 10.4375C15.5635 14.06 12.6235 17 9.00098 17C5.37848 17 2.43848 14.06 2.43848 10.4375C2.43848 6.815 5.37848 3.875 9.00098 3.875C12.6235 3.875 15.5635 6.815 15.5635 10.4375Z"
stroke="#484950"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="dark:stroke-[#888888] stroke-2"
d="M9 6.5V10.25"
stroke="#484950"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
className="dark:stroke-[#888888] stroke-2"
d="M6.75 2H11.25"
stroke="#484950"
strokeMiterlimit="10"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
<span>
{findByUniqueId(mainData, 1503) || "زمان مطالعه"}: {readingTime} {findByUniqueId(mainData, 33) || "دقیقه"}
</span>
</span>
</div>
</div>

<div className="space-y-7">
<h1 className="text-base md:leading-10 md:text-[32px] dark:text-white">{title}</h1>
{/* optionally uncomment excerpt */}
{/* {excerpt && (
<p className="text-[#484950] dark:text-[#868B90] text-sm md:text-[22px] leading-8">
{excerpt}
</p>
)} */}
</div>
</div>
);
}
Loading